commit: ad26b6d593d83ddb539a5c61ee9f639f7b40531e
parent ddb9e90c405369496fdf9e6dfed593eff8d5dc5c
Author: Lain Soykaf <lain@lain.com>
Date: Mon, 20 May 2024 12:30:15 +0400
Merge branch 'develop' of git.pleroma.social:pleroma/pleroma into bump-elixir
Diffstat:
425 files changed, 6564 insertions(+), 2714 deletions(-)
diff --git a/.dialyzer_ignore.exs b/.dialyzer_ignore.exs
@@ -0,0 +1,6 @@
+[
+{"lib/cachex.ex", "Unknown type: Spec.cache/0."},
+{"lib/pleroma/web/plugs/rate_limiter.ex", "The pattern can never match the type {:commit, _} | {:ignore, _}."},
+{"lib/pleroma/web/plugs/rate_limiter.ex", "Function get_scale/2 will never be called."},
+{"lib/pleroma/web/plugs/rate_limiter.ex", "Function initialize_buckets!/1 will never be called."}
+]
diff --git a/.gitignore b/.gitignore
@@ -57,5 +57,6 @@ pleroma.iml
.tool-versions
# Editor temp files
-/*~
-/*#
+*~
+*#
+*.swp
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
@@ -26,9 +26,10 @@ cache: &global_cache_policy
- _build
stages:
- - check-changelog
- build
+ - lint
- test
+ - check-changelog
- benchmark
- deploy
- release
@@ -71,7 +72,7 @@ check-changelog:
tags:
- amd64
-build:
+build-1.12.3:
extends:
- .build_changes_policy
- .using-ci-base
@@ -79,10 +80,20 @@ build:
script:
- mix compile --force
+build-1.15.7-otp-25:
+ extends:
+ - .build_changes_policy
+ - .using-ci-base
+ stage: build
+ image: git.pleroma.social:5050/pleroma/pleroma/ci-base:elixir-1.15
+ allow_failure: true
+ script:
+ - mix compile --force
+
spec-build:
extends:
- .using-ci-base
- stage: test
+ stage: build
rules:
- changes:
- ".gitlab-ci.yml"
@@ -102,7 +113,7 @@ benchmark:
variables:
MIX_ENV: benchmark
services:
- - name: postgres:9.6-alpine
+ - name: postgres:11.22-alpine
alias: postgres
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
script:
@@ -110,7 +121,7 @@ benchmark:
- mix ecto.migrate
- mix pleroma.load_testing
-unit-testing:
+unit-testing-1.12.3:
extends:
- .build_changes_policy
- .using-ci-base
@@ -118,12 +129,11 @@ unit-testing:
cache: &testing_cache_policy
<<: *global_cache_policy
policy: pull
-
- services:
+ services: &testing_services
- name: postgres:13-alpine
alias: postgres
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
- script:
+ script: &testing_script
- mix ecto.create
- mix ecto.migrate
- mix test --cover --preload-modules
@@ -134,49 +144,35 @@ unit-testing:
coverage_format: cobertura
path: coverage.xml
-unit-testing-erratic:
+unit-testing-1.15.7-otp-25:
extends:
- .build_changes_policy
- .using-ci-base
stage: test
- retry: 2
+ image: git.pleroma.social:5050/pleroma/pleroma/ci-base:elixir-1.15-otp25
allow_failure: true
- cache: &testing_cache_policy
- <<: *global_cache_policy
- policy: pull
-
- services:
- - name: postgres:13-alpine
- alias: postgres
- command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
- script:
- - mix ecto.create
- - mix ecto.migrate
- - mix test --only=erratic
+ cache: *testing_cache_policy
+ services: *testing_services
+ script: *testing_script
-unit-testing-rum:
+unit-testing-1.12-erratic:
extends:
- .build_changes_policy
- .using-ci-base
stage: test
+ retry: 2
+ allow_failure: true
cache: *testing_cache_policy
- services:
- - name: git.pleroma.social:5050/pleroma/pleroma/postgres-with-rum-13
- alias: postgres
- command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
- variables:
- <<: *global_variables
- RUM_ENABLED: "true"
+ services: *testing_services
script:
- mix ecto.create
- mix ecto.migrate
- - "mix ecto.migrate --migrations-path priv/repo/optional_migrations/rum_indexing/"
- - mix test --preload-modules
+ - mix test --only=erratic
-lint:
+formatting-1.13:
extends: .build_changes_policy
- image: ¤t_elixir elixir:1.13-alpine
- stage: test
+ image: &formatting_elixir elixir:1.13-alpine
+ stage: lint
cache: *testing_cache_policy
before_script: ¤t_bfr_script
- apk update
@@ -187,24 +183,37 @@ lint:
script:
- mix format --check-formatted
+cycles-1.13:
+ extends: .build_changes_policy
+ image: *formatting_elixir
+ stage: lint
+ cache: {}
+ before_script: *current_bfr_script
+ script:
+ - mix compile
+ - mix xref graph --format cycles --label compile | awk '{print $0} END{exit ($0 != "No cycles found")}'
+
analysis:
extends:
- .build_changes_policy
- .using-ci-base
- stage: test
+ stage: lint
cache: *testing_cache_policy
script:
- mix credo --strict --only=warnings,todo,fixme,consistency,readability
-cycles:
- extends: .build_changes_policy
- image: *current_elixir
- stage: test
- cache: {}
- before_script: *current_bfr_script
+dialyzer:
+ extends:
+ - .build_changes_policy
+ - .using-ci-base
+ stage: lint
+ allow_failure: true
+ when: manual
+ cache: *testing_cache_policy
+ tags:
+ - feld
script:
- - mix compile
- - mix xref graph --format cycles --label compile | awk '{print $0} END{exit ($0 != "No cycles found")}'
+ - mix dialyzer
docs-deploy:
stage: deploy
diff --git a/CHANGELOG.md b/CHANGELOG.md
@@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
+## 2.6.2
+
+### Security
+- MRF StealEmojiPolicy: Sanitize shortcodes (thanks to Hazel K for the report
+
## 2.6.1
### Changed
- - Document maximum supported version of Erlang & Elixir
@@ -62,7 +67,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## 2.5.4
## Security
-- Fix XML External Entity (XXE) loading vulnerability allowing to fetch arbitary files from the server's filesystem
+- Fix XML External Entity (XXE) loading vulnerability allowing to fetch arbitrary files from the server's filesystem
## 2.5.3
@@ -78,7 +83,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## 2.5.4
## Security
-- Fix XML External Entity (XXE) loading vulnerability allowing to fetch arbitary files from the server's filesystem
+- Fix XML External Entity (XXE) loading vulnerability allowing to fetch arbitrary files from the server's filesystem
## 2.5.3
@@ -118,7 +123,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Fix `block_from_stranger` setting
- Fix rel="me"
- Docker images will now run properly
-- Fix inproper content being cached in report content
+- Fix improper content being cached in report content
- Notification filter on object content will not operate on the ones that inherently have no content
- ZWNJ and double dots in links are parsed properly for Plain-text posts
- OTP releases will work on systems with a newer libcrypt
@@ -784,7 +789,7 @@ switched to a new configuration mechanism, however it was not officially removed
- Rate limiter crashes when there is no explicitly specified ip in the config
- 500 errors when no `Accept` header is present if Static-FE is enabled
- Instance panel not being updated immediately due to wrong `Cache-Control` headers
-- Statuses posted with BBCode/Markdown having unncessary newlines in Pleroma-FE
+- Statuses posted with BBCode/Markdown having unnecessary newlines in Pleroma-FE
- OTP: Fix some settings not being migrated to in-database config properly
- No `Cache-Control` headers on attachment/media proxy requests
- Character limit enforcement being off by 1
@@ -1104,10 +1109,10 @@ curl -Lo ./bin/pleroma_ctl 'https://git.pleroma.social/pleroma/pleroma/raw/devel
- Reverse Proxy limiting `max_body_length` was incorrectly defined and only checked `Content-Length` headers which may not be sufficient in some circumstances
### Added
-- Expiring/ephemeral activites. All activities can have expires_at value set, which controls when they should be deleted automatically.
+- Expiring/ephemeral activities. All activities can have expires_at value set, which controls when they should be deleted automatically.
- Mastodon API: in post_status, the expires_in parameter lets you set the number of seconds until an activity expires. It must be at least one hour.
- Mastodon API: all status JSON responses contain a `pleroma.expires_at` item which states when an activity will expire. The value is only shown to the user who created the activity. To everyone else it's empty.
-- Configuration: `ActivityExpiration.enabled` controls whether expired activites will get deleted at the appropriate time. Enabled by default.
+- Configuration: `ActivityExpiration.enabled` controls whether expired activities will get deleted at the appropriate time. Enabled by default.
- Conversations: Add Pleroma-specific conversation endpoints and status posting extensions. Run the `bump_all_conversations` task again to create the necessary data.
- MRF: Support for priming the mediaproxy cache (`Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy`)
- MRF: Support for excluding specific domains from Transparency.
diff --git a/changelog.d/account-rendering-auth-check.fix b/changelog.d/account-rendering-auth-check.fix
@@ -0,0 +1 @@
+Fix authentication check on account rendering when bio is defined
diff --git a/changelog.d/api-docs.skip b/changelog.d/api-docs.skip
diff --git a/changelog.d/atom-leak.skip b/changelog.d/atom-leak.skip
diff --git a/changelog.d/backups-follows.add b/changelog.d/backups-follows.add
@@ -0,0 +1 @@
+Include following/followers in backups
+\ No newline at end of file
diff --git a/changelog.d/bad_inbox_request.change b/changelog.d/bad_inbox_request.change
@@ -0,0 +1 @@
+Invalid activities delivered to the inbox will be rejected with a 400 Bad Request
diff --git a/changelog.d/bandit.change b/changelog.d/bandit.change
@@ -0,0 +1 @@
+Support Bandit as an alternative to Cowboy for the HTTP server.
diff --git a/changelog.d/bookmark-folders.add b/changelog.d/bookmark-folders.add
@@ -0,0 +1 @@
+Allow to group bookmarks in folders
+\ No newline at end of file
diff --git a/changelog.d/bookmark-folders.skip b/changelog.d/bookmark-folders.skip
diff --git a/changelog.d/bugfix-ccworks.fix b/changelog.d/bugfix-ccworks.fix
@@ -0,0 +1 @@
+Fix federation with Convergence AP Bridge
+\ No newline at end of file
diff --git a/changelog.d/card-endpoint.remove b/changelog.d/card-endpoint.remove
@@ -0,0 +1 @@
+Mastodon API: Remove deprecated GET /api/v1/statuses/:id/card endpoint https://github.com/mastodon/mastodon/pull/11213
diff --git a/changelog.d/card-image-description.add b/changelog.d/card-image-description.add
@@ -0,0 +1 @@
+Include image description in status media cards
+\ No newline at end of file
diff --git a/changelog.d/chat-attachment-empty-array.fix b/changelog.d/chat-attachment-empty-array.fix
@@ -0,0 +1 @@
+ChatMessage: Tolerate attachment field set to an empty array
diff --git a/changelog.d/config-stat-symlink.fix b/changelog.d/config-stat-symlink.fix
@@ -0,0 +1 @@
+- Config: Check the permissions of the linked file instead of the symlink
diff --git a/changelog.d/content-length.fix b/changelog.d/content-length.fix
@@ -0,0 +1 @@
+MediaProxy was setting the content-length header which is not permitted by RFC9112§6.2 when we are chunking the reply as it conflicts with the existence of the transfer-encoding header.
diff --git a/changelog.d/deps-bump-2024-01-25.skip b/changelog.d/deps-bump-2024-01-25.skip
diff --git a/changelog.d/description-meilisearch-type.skip b/changelog.d/description-meilisearch-type.skip
diff --git a/changelog.d/dialyzer.skip b/changelog.d/dialyzer.skip
diff --git a/changelog.d/dialyzer2.skip b/changelog.d/dialyzer2.skip
diff --git a/changelog.d/dialyzer3.skip b/changelog.d/dialyzer3.skip
diff --git a/changelog.d/dialyzer4.skip b/changelog.d/dialyzer4.skip
diff --git a/changelog.d/emoji-download-paginate.fix b/changelog.d/emoji-download-paginate.fix
@@ -0,0 +1 @@
+When downloading remote emojis packs, account for pagination
+\ No newline at end of file
diff --git a/changelog.d/emoji-use-v1.fix b/changelog.d/emoji-use-v1.fix
@@ -0,0 +1 @@
+Make remote emoji packs API use specifically the V1 URL. Akkoma does not understand it without V1, and it works either way with normal pleroma, so no reason to not do this
+\ No newline at end of file
diff --git a/changelog.d/exile-bsds.skip b/changelog.d/exile-bsds.skip
diff --git a/changelog.d/exile-freebsd.skip b/changelog.d/exile-freebsd.skip
diff --git a/changelog.d/exile-macos.skip b/changelog.d/exile-macos.skip
diff --git a/changelog.d/exile.skip b/changelog.d/exile.skip
diff --git a/changelog.d/familiar-followers.add b/changelog.d/familiar-followers.add
@@ -0,0 +1 @@
+Implement `/api/v1/accounts/familiar_followers`
+\ No newline at end of file
diff --git a/changelog.d/federator-modules.remove b/changelog.d/federator-modules.remove
@@ -0,0 +1 @@
+Removed support for multiple federator modules as we only support ActivityPub
diff --git a/changelog.d/federator.skip b/changelog.d/federator.skip
diff --git a/changelog.d/fep-2c59.add b/changelog.d/fep-2c59.add
@@ -0,0 +1 @@
+Implement FEP-2c59, add "webfinger" to user actor
+\ No newline at end of file
diff --git a/changelog.d/ffmpeg-limiter.add b/changelog.d/ffmpeg-limiter.add
@@ -0,0 +1 @@
+Framegrabs with ffmpeg will execute with a 5 second timeout and cache the URLs of failures with a TTL of 15 minutes to prevent excessive retries.
diff --git a/changelog.d/finch_redirects.fix b/changelog.d/finch_redirects.fix
@@ -0,0 +1 @@
+Following HTTP Redirects when the HTTP Adapter is Finch
diff --git a/changelog.d/fix-bookmark-folder-tests.skip b/changelog.d/fix-bookmark-folder-tests.skip
diff --git a/changelog.d/fix-duplicate-inbox-deliveries.fix b/changelog.d/fix-duplicate-inbox-deliveries.fix
diff --git a/changelog.d/force-mention-mrf.add b/changelog.d/force-mention-mrf.add
@@ -0,0 +1 @@
+Add ForceMention MRF
+\ No newline at end of file
diff --git a/changelog.d/framegrabs.fix b/changelog.d/framegrabs.fix
@@ -0,0 +1 @@
+Video framegrabs were not working correctly after the change to use Exile to execute ffmpeg
diff --git a/changelog.d/group-actor.add b/changelog.d/group-actor.add
@@ -0,0 +1 @@
+Implement group actors
diff --git a/changelog.d/gun-logs.skip b/changelog.d/gun-logs.skip
diff --git a/changelog.d/gun_pool.fix b/changelog.d/gun_pool.fix
@@ -0,0 +1 @@
+Fix logic error in Gun connection pooling which prevented retries even when the worker was launched with retry = true
diff --git a/changelog.d/gun_pool2.fix b/changelog.d/gun_pool2.fix
@@ -0,0 +1 @@
+Connection pool errors when publishing an activity is a soft-error that will be retried shortly.
diff --git a/changelog.d/gun_pool3.skip b/changelog.d/gun_pool3.skip
diff --git a/changelog.d/handle_object_fetch_failures.change b/changelog.d/handle_object_fetch_failures.change
@@ -0,0 +1 @@
+Remote object fetch failures will prevent the object fetch job from retrying if the object request returns 401, 403, 404, 410, or exceeds the maximum thread depth.
diff --git a/changelog.d/instance-contact-account.add b/changelog.d/instance-contact-account.add
@@ -0,0 +1 @@
+Add contact account to InstanceView
+\ No newline at end of file
diff --git a/changelog.d/instance-defdelegates.skip b/changelog.d/instance-defdelegates.skip
diff --git a/changelog.d/instance-rules.add b/changelog.d/instance-rules.add
@@ -0,0 +1 @@
+Add instance rules
+\ No newline at end of file
diff --git a/changelog.d/instance-v2.add b/changelog.d/instance-v2.add
@@ -0,0 +1 @@
+Implement /api/v2/instance route
+\ No newline at end of file
diff --git a/changelog.d/instance-v2.skip b/changelog.d/instance-v2.skip
diff --git a/changelog.d/issue-3241.fix b/changelog.d/issue-3241.fix
@@ -0,0 +1 @@
+Handle cases when users.inbox is nil.
diff --git a/changelog.d/link-verification.add b/changelog.d/link-verification.add
@@ -0,0 +1 @@
+Verify profile link ownership with rel="me"
+\ No newline at end of file
diff --git a/changelog.d/local-webfinger.fix b/changelog.d/local-webfinger.fix
@@ -0,0 +1 @@
+Use correct domain for fqn and InstanceView
+\ No newline at end of file
diff --git a/changelog.d/mark-read.fix b/changelog.d/mark-read.fix
@@ -0,0 +1 @@
+The query for marking notifications as read has been simplified
diff --git a/changelog.d/mastodon_api_v2.add b/changelog.d/mastodon_api_v2.add
@@ -0,0 +1 @@
+Add new parameters to /api/v2/instance: configuration[accounts][max_pinned_statuses] and configuration[statuses][characters_reserved_per_url]
diff --git a/changelog.d/mastodon_directory.fix b/changelog.d/mastodon_directory.fix
@@ -0,0 +1 @@
+Mastodon API /api/v1/directory: Fix listing directory contents when not authenticated
diff --git a/changelog.d/memleak.fix b/changelog.d/memleak.fix
@@ -0,0 +1 @@
+Fix a memory leak caused by Websocket connections that would not enter a state where a full garbage collection run could be triggered.
diff --git a/changelog.d/mergeback-2.6.2.skip b/changelog.d/mergeback-2.6.2.skip
diff --git a/changelog.d/missing-mrfs.add b/changelog.d/missing-mrfs.add
@@ -0,0 +1 @@
+Startup detection for configured MRF modules that are missing or incorrectly defined
diff --git a/changelog.d/mrf-regex-error.fix b/changelog.d/mrf-regex-error.fix
@@ -0,0 +1 @@
+MRF: Log sensible error for subdomains_regex
diff --git a/changelog.d/mrf-steal-emoji-extname.fix b/changelog.d/mrf-steal-emoji-extname.fix
@@ -0,0 +1 @@
+MRF.StealEmojiPolicy: Properly add fallback extension to filenames missing one
diff --git a/changelog.d/mrf_hashtags.fix b/changelog.d/mrf_hashtags.fix
@@ -0,0 +1 @@
+Federated timeline removal of hashtags via MRF HashtagPolicy
diff --git a/changelog.d/nil-content-map.fix b/changelog.d/nil-content-map.fix
@@ -0,0 +1 @@
+Support objects with a null contentMap (firefish)
diff --git a/changelog.d/notifications-index.fix b/changelog.d/notifications-index.fix
@@ -0,0 +1 @@
+Fix notifications query which was not using the index properly
diff --git a/changelog.d/notifications.fix b/changelog.d/notifications.fix
@@ -0,0 +1 @@
+Notifications: improve performance by filtering on users table instead of activities table
+\ No newline at end of file
diff --git a/changelog.d/oauth-nickname.skip b/changelog.d/oauth-nickname.skip
@@ -0,0 +1 @@
+Use User.full_nickname/1 in oauth html template
+\ No newline at end of file
diff --git a/changelog.d/otp26.add b/changelog.d/otp26.add
@@ -0,0 +1 @@
+Support for Erlang OTP 26
diff --git a/changelog.d/postgres-jit.change b/changelog.d/postgres-jit.change
@@ -0,0 +1 @@
+Disable jit by default for PostgreSQL
diff --git a/changelog.d/public-polls.add b/changelog.d/public-polls.add
@@ -0,0 +1 @@
+Expose nonAnonymous field from Smithereen polls
+\ No newline at end of file
diff --git a/changelog.d/publisher_discard.change b/changelog.d/publisher_discard.change
@@ -0,0 +1 @@
+Activity publishing failures will prevent the job from retrying if the publishing request returns a 403 or 410
diff --git a/changelog.d/publisher_log.change b/changelog.d/publisher_log.change
@@ -0,0 +1 @@
+Publisher errors will now emit logs indicating the inbox that was not available for delivery.
diff --git a/changelog.d/qtfaststart.fix b/changelog.d/qtfaststart.fix
@@ -0,0 +1 @@
+MediaProxy Preview failures prevented when encountering certain video files
diff --git a/changelog.d/receiverworker-error-handling.fix b/changelog.d/receiverworker-error-handling.fix
@@ -0,0 +1 @@
+ReceiverWorker: Make sure non-{:ok, _} is returned as {:error, …}
+\ No newline at end of file
diff --git a/changelog.d/remote-fetcher-error.skip b/changelog.d/remote-fetcher-error.skip
diff --git a/changelog.d/rich_media.fix b/changelog.d/rich_media.fix
@@ -0,0 +1 @@
+Rich Media Preview cache eviction when the activity is updated.
diff --git a/changelog.d/rich_media_refactor.change b/changelog.d/rich_media_refactor.change
@@ -0,0 +1 @@
+Refactored Rich Media to cache the content in the database. Fetching operations that could block status rendering have been eliminated.
diff --git a/changelog.d/rich_media_tests.skip b/changelog.d/rich_media_tests.skip
diff --git a/changelog.d/tesla.deps b/changelog.d/tesla.deps
@@ -0,0 +1 @@
+Update Tesla HTTP client middleware to 1.8.0
diff --git a/changelog.d/test-improvements.skip b/changelog.d/test-improvements.skip
diff --git a/changelog.d/transient-validators-defaults.change b/changelog.d/transient-validators-defaults.change
@@ -0,0 +1 @@
+Set default values on validators for transient objects (attachment, poll options)
diff --git a/changelog.d/typo.skip b/changelog.d/typo.skip
diff --git a/changelog.d/web_push_filtered.fix b/changelog.d/web_push_filtered.fix
@@ -0,0 +1 @@
+Web Push notifications are no longer generated for muted/blocked threads and users.
diff --git a/changelog.d/websocket-refactor.change b/changelog.d/websocket-refactor.change
@@ -0,0 +1 @@
+Refactor the Mastodon /api/v1/streaming websocket handler to use Phoenix.Socket.Transport
diff --git a/ci/build_and_push.sh b/ci/build_and_push.sh
@@ -1 +0,0 @@
-docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t git.pleroma.social:5050/pleroma/pleroma/ci-base:latest --push .
diff --git a/ci/Dockerfile b/ci/elixir-1.12/Dockerfile
diff --git a/ci/elixir-1.12/build_and_push.sh b/ci/elixir-1.12/build_and_push.sh
@@ -0,0 +1 @@
+docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t git.pleroma.social:5050/pleroma/pleroma/ci-base:elixir-1.12 --push .
diff --git a/ci/elixir-1.15-otp25/Dockerfile b/ci/elixir-1.15-otp25/Dockerfile
@@ -0,0 +1,8 @@
+FROM elixir:1.15.7-otp-25
+
+# Single RUN statement, otherwise intermediate images are created
+# https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#run
+RUN apt-get update &&\
+ apt-get install -y libmagic-dev cmake libimage-exiftool-perl ffmpeg &&\
+ mix local.hex --force &&\
+ mix local.rebar --force
diff --git a/ci/elixir-1.15-otp25/build_and_push.sh b/ci/elixir-1.15-otp25/build_and_push.sh
@@ -0,0 +1 @@
+docker buildx build --platform linux/amd64 -t git.pleroma.social:5050/pleroma/pleroma/ci-base:elixir-1.15-otp25 --push .
diff --git a/ci/postgres_rum/Dockerfile b/ci/postgres-with-rum-13/Dockerfile
diff --git a/ci/postgres_rum/build_and_push.sh b/ci/postgres-with-rum-13/build_and_push.sh
diff --git a/config/benchmark.exs b/config/benchmark.exs
@@ -79,6 +79,10 @@ IO.puts("RUM enabled: #{rum_enabled}")
config :pleroma, Pleroma.ReverseProxy.Client, Pleroma.ReverseProxy.ClientMock
+config :pleroma, Pleroma.Application,
+ background_migrators: false,
+ streamer_registry: false
+
if File.exists?("./config/benchmark.secret.exs") do
import_config "benchmark.secret.exs"
else
diff --git a/config/config.exs b/config/config.exs
@@ -114,14 +114,7 @@ config :pleroma, :uri_schemes,
config :pleroma, Pleroma.Web.Endpoint,
url: [host: "localhost"],
http: [
- ip: {127, 0, 0, 1},
- dispatch: [
- {:_,
- [
- {"/api/v1/streaming", Pleroma.Web.MastodonAPI.WebsocketHandler, []},
- {:_, Plug.Cowboy.Handler, {Pleroma.Web.Endpoint, []}}
- ]}
- ]
+ ip: {127, 0, 0, 1}
],
protocol: "https",
secret_key_base: "aK4Abxf29xU9TTDKre9coZPUgevcVCFQJe/5xP/7Lt4BEif6idBIbjupVbOrbKxl",
@@ -192,9 +185,6 @@ config :pleroma, :instance,
federating: true,
federation_incoming_replies_max_depth: 100,
federation_reachability_timeout_days: 7,
- federation_publisher_modules: [
- Pleroma.Web.ActivityPub.Publisher
- ],
allow_relay: true,
public: true,
quarantined_instances: [],
@@ -425,6 +415,10 @@ config :pleroma, :mrf_follow_bot, follower_nickname: nil
config :pleroma, :mrf_inline_quote, template: "<bdi>RT:</bdi> {url}"
+config :pleroma, :mrf_force_mention,
+ mention_parent: true,
+ mention_quoted: true
+
config :pleroma, :rich_media,
enabled: true,
ignore_hosts: [],
@@ -434,7 +428,11 @@ config :pleroma, :rich_media,
Pleroma.Web.RichMedia.Parsers.OEmbed
],
failure_backoff: 60_000,
- ttl_setters: [Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl]
+ ttl_setters: [
+ Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl,
+ Pleroma.Web.RichMedia.Parser.TTL.Opengraph
+ ],
+ max_body: 5_000_000
config :pleroma, :media_proxy,
enabled: false,
@@ -581,7 +579,8 @@ config :pleroma, Oban,
attachments_cleanup: 1,
new_users_digest: 1,
mute_expire: 5,
- search_indexing: 10
+ search_indexing: 10,
+ rich_media_expiration: 2
],
plugins: [Oban.Plugins.Pruner],
crontab: [
@@ -805,7 +804,7 @@ config :pleroma, :modules, runtime_dir: "instance/modules"
config :pleroma, configurable_from_database: false
config :pleroma, Pleroma.Repo,
- parameters: [gin_fuzzy_search_limit: "500"],
+ parameters: [gin_fuzzy_search_limit: "500", jit: "off"],
prepare: :unnamed
config :pleroma, :connections_pool,
@@ -907,6 +906,15 @@ config :pleroma, Pleroma.Search.Meilisearch,
private_key: nil,
initial_indexing_chunk_size: 100_000
+config :pleroma, Pleroma.Application,
+ background_migrators: true,
+ internal_fetch: true,
+ load_custom_modules: true,
+ max_restarts: 3,
+ streamer_registry: true
+
+config :pleroma, Pleroma.Uploaders.Uploader, timeout: 30_000
+
# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{Mix.env()}.exs"
diff --git a/config/description.exs b/config/description.exs
@@ -567,6 +567,20 @@ config :pleroma, :config_description, [
]
},
%{
+ key: :status_page,
+ type: :string,
+ description: "A page where people can see the status of the server during an outage",
+ suggestions: [
+ "https://status.pleroma.example.org"
+ ]
+ },
+ %{
+ key: :contact_username,
+ type: :string,
+ description: "Instance owner username",
+ suggestions: ["admin"]
+ },
+ %{
key: :limit,
type: :integer,
description: "Posts character limit (CW/Subject included in the counter)",
@@ -1444,7 +1458,7 @@ config :pleroma, :config_description, [
label: "Subject line behavior",
type: :string,
description: "Allows changing the default behaviour of subject lines in replies.
- `email`: copy and preprend re:, as in email,
+ `email`: copy and prepend re:, as in email,
`masto`: copy verbatim, as in Mastodon,
`noop`: don't copy the subject.",
suggestions: ["email", "masto", "noop"]
@@ -3096,7 +3110,7 @@ config :pleroma, :config_description, [
key: :max_waiting,
type: :integer,
description:
- "Maximum number of requests waiting for other requests to finish. After this number is reached, the pool will start returning errrors when a new request is made",
+ "Maximum number of requests waiting for other requests to finish. After this number is reached, the pool will start returning errors when a new request is made",
suggestions: [10]
},
%{
@@ -3362,7 +3376,7 @@ config :pleroma, :config_description, [
%{
key: :purge_after_days,
type: :integer,
- description: "Remove backup achives after N days",
+ description: "Remove backup archives after N days",
suggestions: [30]
},
%{
@@ -3508,7 +3522,7 @@ config :pleroma, :config_description, [
},
%{
key: :initial_indexing_chunk_size,
- type: :int,
+ type: :integer,
description:
"Amount of posts in a batch when running the initial indexing operation. Should probably not be more than 100000" <>
" since there's a limit on maximum insert size",
diff --git a/config/dev.exs b/config/dev.exs
@@ -8,8 +8,7 @@ import Config
# with brunch.io to recompile .js and .css sources.
config :pleroma, Pleroma.Web.Endpoint,
http: [
- port: 4000,
- protocol_options: [max_request_line_length: 8192, max_header_value_length: 8192]
+ port: 4000
],
protocol: "http",
debug_errors: true,
diff --git a/config/test.exs b/config/test.exs
@@ -49,7 +49,7 @@ config :pleroma, Pleroma.Repo,
hostname: System.get_env("DB_HOST") || "localhost",
port: System.get_env("DB_PORT") || "5432",
pool: Ecto.Adapters.SQL.Sandbox,
- pool_size: 50
+ pool_size: System.schedulers_online() * 2
config :pleroma, :dangerzone, override_repo_pool_size: true
@@ -61,7 +61,8 @@ config :tesla, adapter: Tesla.Mock
config :pleroma, :rich_media,
enabled: false,
ignore_hosts: [],
- ignore_tld: ["local", "localdomain", "lan"]
+ ignore_tld: ["local", "localdomain", "lan"],
+ max_body: 2_000_000
config :pleroma, :instance,
multi_factor_authentication: [
@@ -162,6 +163,20 @@ peer_module =
config :pleroma, Pleroma.Cluster, peer_module: peer_module
+config :pleroma, Pleroma.Application,
+ background_migrators: false,
+ internal_fetch: false,
+ load_custom_modules: false,
+ max_restarts: 100,
+ streamer_registry: false,
+ test_http_pools: true
+
+config :pleroma, Pleroma.Uploaders.Uploader, timeout: 1_000
+
+config :pleroma, Pleroma.Emoji.Loader, test_emoji: true
+
+config :pleroma, Pleroma.Web.RichMedia.Backfill, provider: Pleroma.Web.RichMedia.Backfill
+
if File.exists?("./config/test.secret.exs") do
import_config "test.secret.exs"
else
diff --git a/docs/administration/CLI_tasks/config.md b/docs/administration/CLI_tasks/config.md
@@ -1,4 +1,4 @@
-# Transfering the config to/from the database
+# Transferring the config to/from the database
{! backend/administration/CLI_tasks/general_cli_task_info.include !}
@@ -34,7 +34,7 @@
Options:
-- `<path>` - where to save migrated config. E.g. `--path=/tmp`. If file saved into non standart folder, you must manually copy file into directory where Pleroma can read it. For OTP install path will be `PLEROMA_CONFIG_PATH` or `/etc/pleroma`. For installation from source - `config` directory in the pleroma folder.
+- `<path>` - where to save migrated config. E.g. `--path=/tmp`. If file saved into non-standard folder, you must manually copy file into directory where Pleroma can read it. For OTP install path will be `PLEROMA_CONFIG_PATH` or `/etc/pleroma`. For installation from source - `config` directory in the pleroma folder.
- `<env>` - environment, for which is migrated config. By default is `prod`.
- To delete transferred settings from database optional flag `-d` can be used
diff --git a/docs/administration/backup.md b/docs/administration/backup.md
@@ -31,7 +31,7 @@
1. Optionally you can remove the users of your instance. This will trigger delete requests for their accounts and posts. Note that this is 'best effort' and doesn't mean that all traces of your instance will be gone from the fediverse.
* You can do this from the admin-FE where you can select all local users and delete the accounts using the *Moderate multiple users* dropdown.
- * You can also list local users and delete them individualy using the CLI tasks for [Managing users](./CLI_tasks/user.md).
+ * You can also list local users and delete them individually using the CLI tasks for [Managing users](./CLI_tasks/user.md).
2. Stop the Pleroma service `systemctl stop pleroma`
3. Disable pleroma from systemd `systemctl disable pleroma`
4. Remove the files and folders you created during installation (see installation guide). This includes the pleroma, nginx and systemd files and folders.
diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md
@@ -154,14 +154,15 @@ To add configuration to your config file, you can copy it from the base config.
* `Pleroma.Web.ActivityPub.MRF.MentionPolicy`: Drops posts mentioning configurable users. (See [`:mrf_mention`](#mrf_mention)).
* `Pleroma.Web.ActivityPub.MRF.VocabularyPolicy`: Restricts activities to a configured set of vocabulary. (See [`:mrf_vocabulary`](#mrf_vocabulary)).
* `Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy`: Rejects or delists posts based on their age when received. (See [`:mrf_object_age`](#mrf_object_age)).
- * `Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy`: Sets a default expiration on all posts made by users of the local instance. Requires `Pleroma.Workers.PurgeExpiredActivity` to be enabled for processing the scheduled delections.
+ * `Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy`: Sets a default expiration on all posts made by users of the local instance. Requires `Pleroma.Workers.PurgeExpiredActivity` to be enabled for processing the scheduled deletions.
* `Pleroma.Web.ActivityPub.MRF.ForceBotUnlistedPolicy`: Makes all bot posts to disappear from public timelines.
* `Pleroma.Web.ActivityPub.MRF.FollowBotPolicy`: Automatically follows newly discovered users from the specified bot account. Local accounts, locked accounts, and users with "#nobot" in their bio are respected and excluded from being followed.
* `Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy`: Drops follow requests from followbots. Users can still allow bots to follow them by first following the bot.
* `Pleroma.Web.ActivityPub.MRF.KeywordPolicy`: Rejects or removes from the federated timeline or replaces keywords. (See [`:mrf_keyword`](#mrf_keyword)).
* `Pleroma.Web.ActivityPub.MRF.ForceMentionsInContent`: Forces every mentioned user to be reflected in the post content.
* `Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy`: Forces quote post URLs to be reflected in the message content inline.
- * `Pleroma.Web.ActivityPub.MRF.QuoteToLinkTagPolicy`: Force a Link tag for posts quoting another post. (may break outgoing federation of quote posts with older Pleroma versions)
+ * `Pleroma.Web.ActivityPub.MRF.QuoteToLinkTagPolicy`: Force a Link tag for posts quoting another post. (may break outgoing federation of quote posts with older Pleroma versions).
+ * `Pleroma.Web.ActivityPub.MRF.ForceMention`: Forces posts to include a mention of the author of parent post or the author of quoted post.
* `transparency`: Make the content of your Message Rewrite Facility settings public (via nodeinfo).
* `transparency_exclusions`: Exclude specific instance names from MRF transparency. The use of the exclusions feature will be disclosed in nodeinfo as a boolean value.
@@ -272,6 +273,10 @@ Notes:
#### :mrf_inline_quote
* `template`: The template to append to the post. `{url}` will be replaced with the actual link to the quoted post. Default: `<bdi>RT:</bdi> {url}`
+#### :mrf_force_mention
+* `mention_parent`: Whether to append mention of parent post author
+* `mention_quoted`: Whether to append mention of parent quoted author
+
### :activitypub
* `unfollow_blocked`: Whether blocks result in people getting unfollowed
* `outgoing_blocks`: Whether to federate blocks to other instances
@@ -506,7 +511,7 @@ config :pleroma, :rate_limit,
Means that:
1. In 60 seconds, 15 authentication attempts can be performed from the same IP address.
-2. In 1 second, 10 search requests can be performed from the same IP adress by unauthenticated users, while authenticated users can perform 30 search requests per second.
+2. In 1 second, 10 search requests can be performed from the same IP address by unauthenticated users, while authenticated users can perform 30 search requests per second.
Supported rate limiters:
@@ -1081,7 +1086,7 @@ config :pleroma, Pleroma.Formatter,
## :configurable_from_database
-Boolean, enables/disables in-database configuration. Read [Transfering the config to/from the database](../administration/CLI_tasks/config.md) for more information.
+Boolean, enables/disables in-database configuration. Read [Transferring the config to/from the database](../administration/CLI_tasks/config.md) for more information.
## :database_config_whitelist
@@ -1142,7 +1147,7 @@ Control favicons for instances.
!!! note
Requires enabled email
-* `:purge_after_days` an integer, remove backup achives after N days.
+* `:purge_after_days` an integer, remove backup achieves after N days.
* `:limit_days` an integer, limit user to export not more often than once per N days.
* `:dir` a string with a path to backup temporary directory or `nil` to let Pleroma choose temporary directory in the following order:
1. the directory named by the TMPDIR environment variable
diff --git a/docs/configuration/custom_emoji.md b/docs/configuration/custom_emoji.md
@@ -29,7 +29,7 @@ foo, /emoji/custom/foo.png
The files should be PNG (APNG is okay with `.png` for `image/png` Content-type) and under 50kb for compatibility with mastodon.
-Default file extentions and locations for emojis are set in `config.exs`. To use different locations or file-extentions, add the `shortcode_globs` to your secrets file (`prod.secret.exs` or `dev.secret.exs`) and edit it. Note that not all fediverse-software will show emojis with other file extentions:
+Default file extensions and locations for emojis are set in `config.exs`. To use different locations or file-extensions, add the `shortcode_globs` to your secrets file (`prod.secret.exs` or `dev.secret.exs`) and edit it. Note that not all fediverse-software will show emojis with other file extensions:
```elixir
config :pleroma, :emoji, shortcode_globs: ["/emoji/custom/**/*.png", "/emoji/custom/**/*.gif"]
```
diff --git a/docs/configuration/i2p.md b/docs/configuration/i2p.md
@@ -1,4 +1,4 @@
-# I2P Federation and Accessability
+# I2P Federation and Accessibility
This guide is going to focus on the Pleroma federation aspect. The actual installation is neatly explained in the official documentation, and more likely to remain up-to-date.
It might be added to this guide if there will be a need for that.
diff --git a/docs/configuration/onion_federation.md b/docs/configuration/onion_federation.md
@@ -29,7 +29,7 @@ HiddenServiceDir /var/lib/tor/pleroma_hidden_service/
HiddenServicePort 80 127.0.0.1:8099
HiddenServiceVersion 3 # Remove if Tor version is below 0.3 ( tor --version )
```
-Restart Tor to generate an adress:
+Restart Tor to generate an address:
```
systemctl restart tor@default.service
```
diff --git a/docs/configuration/optimizing_beam.md b/docs/configuration/optimizing_beam.md
@@ -1,6 +1,6 @@
# Optimizing the BEAM
-Pleroma is built upon the Erlang/OTP VM known as BEAM. The BEAM VM is highly optimized for latency, but this has drawbacks in environments without dedicated hardware. One of the tricks used by the BEAM VM is [busy waiting](https://en.wikipedia.org/wiki/Busy_waiting). This allows the application to pretend to be busy working so the OS kernel does not pause the application process and switch to another process waiting for the CPU to execute its workload. It does this by spinning for a period of time which inflates the apparent CPU usage of the application so it is immediately ready to execute another task. This can be observed with utilities like **top(1)** which will show consistently high CPU usage for the process. Switching between procesess is a rather expensive operation and also clears CPU caches further affecting latency and performance. The goal of busy waiting is to avoid this penalty.
+Pleroma is built upon the Erlang/OTP VM known as BEAM. The BEAM VM is highly optimized for latency, but this has drawbacks in environments without dedicated hardware. One of the tricks used by the BEAM VM is [busy waiting](https://en.wikipedia.org/wiki/Busy_waiting). This allows the application to pretend to be busy working so the OS kernel does not pause the application process and switch to another process waiting for the CPU to execute its workload. It does this by spinning for a period of time which inflates the apparent CPU usage of the application so it is immediately ready to execute another task. This can be observed with utilities like **top(1)** which will show consistently high CPU usage for the process. Switching between processes is a rather expensive operation and also clears CPU caches further affecting latency and performance. The goal of busy waiting is to avoid this penalty.
This strategy is very successful in making a performant and responsive application, but is not desirable on Virtual Machines or hardware with few CPU cores. Pleroma instances are often deployed on the same server as the required PostgreSQL database which can lead to situations where the Pleroma application is holding the CPU in a busy-wait loop and as a result the database cannot process requests in a timely manner. The fewer CPUs available, the more this problem is exacerbated. The latency is further amplified by the OS being installed on a Virtual Machine as the Hypervisor uses CPU time-slicing to pause the entire OS and switch between other tasks.
diff --git a/docs/configuration/postgresql.md b/docs/configuration/postgresql.md
@@ -22,7 +22,7 @@ config :pleroma, Pleroma.Repo,
]
```
-A more detailed explaination of the issue can be found at <https://blog.soykaf.com/post/postgresql-elixir-troubles/>.
+A more detailed explanation of the issue can be found at <https://blog.soykaf.com/post/postgresql-elixir-troubles/>.
## Example configurations
diff --git a/docs/configuration/search.md b/docs/configuration/search.md
@@ -38,7 +38,7 @@ indexes faster when it can process many posts in a single batch.
Information about setting up meilisearch can be found in the
[official documentation](https://docs.meilisearch.com/learn/getting_started/installation.html).
You probably want to start it with `MEILI_NO_ANALYTICS=true` environment variable to disable analytics.
-At least version 0.25.0 is required, but you are strongly adviced to use at least 0.26.0, as it introduces
+At least version 0.25.0 is required, but you are strongly advised to use at least 0.26.0, as it introduces
the `--enable-auto-batching` option which drastically improves performance. Without this option, the search
is hardly usable on a somewhat big instance.
@@ -61,7 +61,7 @@ You will see a "Default Admin API Key", this is the key you actually put into yo
### Initial indexing
-After setting up the configuration, you'll want to index all of your already existsing posts. Only public posts are indexed. You'll only
+After setting up the configuration, you'll want to index all of your already existing posts. Only public posts are indexed. You'll only
have to do it one time, but it might take a while, depending on the amount of posts your instance has seen. This is also a fairly RAM
consuming process for `meilisearch`, and it will take a lot of RAM when running if you have a lot of posts (seems to be around 5G for ~1.2
million posts while idle and up to 7G while indexing initially, but your experience may be different).
diff --git a/docs/development/API/admin_api.md b/docs/development/API/admin_api.md
@@ -303,7 +303,7 @@ Removes the user(s) from follower recommendations.
## `GET /api/v1/pleroma/admin/users/:nickname_or_id`
-### Retrive the details of a user
+### Retrieve the details of a user
- Params:
- `nickname` or `id`
@@ -313,7 +313,7 @@ Removes the user(s) from follower recommendations.
## `GET /api/v1/pleroma/admin/users/:nickname_or_id/statuses`
-### Retrive user's latest statuses
+### Retrieve user's latest statuses
- Params:
- `nickname` or `id`
@@ -337,7 +337,7 @@ Removes the user(s) from follower recommendations.
## `GET /api/v1/pleroma/admin/instances/:instance/statuses`
-### Retrive instance's latest statuses
+### Retrieve instance's latest statuses
- Params:
- `instance`: instance name
@@ -377,7 +377,7 @@ It may take some time.
## `GET /api/v1/pleroma/admin/statuses`
-### Retrives all latest statuses
+### Retrieves all latest statuses
- Params:
- *optional* `page_size`: number of statuses to return (default is `20`)
@@ -541,7 +541,7 @@ Response:
## `PATCH /api/v1/pleroma/admin/users/force_password_reset`
-### Force passord reset for a user with a given nickname
+### Force password reset for a user with a given nickname
- Params:
- `nicknames`
@@ -1751,3 +1751,53 @@ Note that this differs from the Mastodon API variant: Mastodon API only returns
```json
{}
```
+
+
+## `GET /api/v1/pleroma/admin/rules`
+
+### List rules
+
+- Response: JSON, list of rules
+
+```json
+[
+ {
+ "id": "1",
+ "priority": 1,
+ "text": "There are no rules",
+ "hint": null
+ }
+]
+```
+
+## `POST /api/v1/pleroma/admin/rules`
+
+### Create a rule
+
+- Params:
+ - `text`: string, required, rule content
+ - `hint`: string, optional, rule description
+ - `priority`: integer, optional, rule ordering priority
+
+- Response: JSON, a single rule
+
+## `PATCH /api/v1/pleroma/admin/rules/:id`
+
+### Update a rule
+
+- Params:
+ - `text`: string, optional, rule content
+ - `hint`: string, optional, rule description
+ - `priority`: integer, optional, rule ordering priority
+
+- Response: JSON, a single rule
+
+## `DELETE /api/v1/pleroma/admin/rules/:id`
+
+### Delete a rule
+
+- Response: JSON, empty object
+
+```json
+{}
+```
diff --git a/docs/development/API/differences_in_mastoapi_responses.md b/docs/development/API/differences_in_mastoapi_responses.md
@@ -1,6 +1,6 @@
# Differences in Mastodon API responses from vanilla Mastodon
-A Pleroma instance can be identified by "<Mastodon version> (compatible; Pleroma <version>)" present in `version` field in response from `/api/v1/instance`
+A Pleroma instance can be identified by "<Mastodon version> (compatible; Pleroma <version>)" present in `version` field in response from `/api/v1/instance` and `/api/v2/instance`
## Flake IDs
@@ -39,6 +39,9 @@ Has these additional fields under the `pleroma` object:
- `emoji_reactions`: A list with emoji / reaction maps. The format is `{name: "☕", count: 1, me: true}`. Contains no information about the reacting users, for that use the `/statuses/:id/reactions` endpoint.
- `parent_visible`: If the parent of this post is visible to the user or not.
- `pinned_at`: a datetime (iso8601) when status was pinned, `null` otherwise.
+- `quotes_count`: the count of status quotes.
+- `non_anonymous`: true if the source post specifies the poll results are not anonymous. Currently only implemented by Smithereen.
+- `bookmark_folder`: the ID of the folder bookmark is stored within (if any).
The `GET /api/v1/statuses/:id/source` endpoint additionally has the following attributes:
@@ -64,6 +67,12 @@ Some apps operate under the assumption that no more than 4 attachments can be re
Pleroma does not process remote images and therefore cannot include fields such as `meta` and `blurhash`. It does not support focal points or aspect ratios. The frontend is expected to handle it.
+## Bookmarks
+
+The `GET /api/v1/bookmarks` endpoint accepts optional parameter `folder_id` for bookmark folder ID.
+
+The `POST /api/v1/statuses/:id/bookmark` endpoint accepts optional parameter `folder_id` for bookmark folder ID.
+
## Accounts
The `id` parameter can also be the `nickname` of the user. This only works in these endpoints, not the deeper nested ones for following etc.
@@ -304,19 +313,27 @@ Has these additional parameters (which are the same as in Pleroma-API):
`GET /api/v1/instance` has additional fields
- `max_toot_chars`: The maximum characters per post
+- `max_media_attachments`: Maximum number of post media attachments
- `chat_limit`: The maximum characters per chat message
- `description_limit`: The maximum characters per image description
- `poll_limits`: The limits of polls
+- `shout_limit`: The maximum characters per Shoutbox message
- `upload_limit`: The maximum upload file size
- `avatar_upload_limit`: The same for avatars
- `background_upload_limit`: The same for backgrounds
- `banner_upload_limit`: The same for banners
- `background_image`: A background image that frontends can use
+- `pleroma.metadata.account_activation_required`: Whether users are required to confirm their emails before signing in
+- `pleroma.metadata.birthday_required`: Whether users are required to provide their birth day when signing in
+- `pleroma.metadata.birthday_min_age`: The minimum user age (in days)
- `pleroma.metadata.features`: A list of supported features
- `pleroma.metadata.federation`: The federation restrictions of this instance
- `pleroma.metadata.fields_limits`: A list of values detailing the length and count limitation for various instance-configurable fields.
- `pleroma.metadata.post_formats`: A list of the allowed post format types
-- `vapid_public_key`: The public key needed for push messages
+- `pleroma.stats.mau`: Monthly active user count
+- `pleroma.vapid_public_key`: The public key needed for push messages
+
+In, `GET /api/v2/instance` Pleroma-specific fields are all moved into `pleroma` object. `max_toot_chars`, `poll_limits` and `upload_limit` are replaced with their MastoAPI counterparts.
## Push Subscription
diff --git a/docs/development/API/pleroma_api.md b/docs/development/API/pleroma_api.md
@@ -129,7 +129,7 @@ The `/api/v1/pleroma/*` path is backwards compatible with `/api/pleroma/*` (`/ap
* method: `GET`
* Authentication: required
* OAuth scope: `write:security`
-* Response: JSON. Returns `{"codes": codes}`when successful, otherwise HTTP 422 `{"error": "[error message]"}`
+* Response: JSON. Returns `{"codes": codes}` when successful, otherwise HTTP 422 `{"error": "[error message]"}`
## `/api/v1/pleroma/admin/`
See [Admin-API](admin_api.md)
@@ -251,6 +251,15 @@ See [Admin-API](admin_api.md)
]
```
+
+## `/api/v1/pleroma/accounts/:id/endorsements`
+### Returns users endorsed by a user
+* Method `GET`
+* Authentication: not required
+* Params:
+ * `id`: the id of the account for whom to return results
+* Response: JSON, returns a list of Mastodon Account entities
+
## `/api/v1/pleroma/accounts/update_*`
### Set and clear account avatar, banner, and background
@@ -266,6 +275,60 @@ See [Admin-API](admin_api.md)
* Authentication: not required
* Response: 204 No Content
+## `/api/v1/pleroma/statuses/:id/quotes`
+### Gets quotes for a given status
+* Method `GET`
+* Authentication: not required
+* Params:
+ * `id`: the id of the status
+* Response: JSON, returns a list of Mastodon Status entities
+
+## `GET /api/v1/pleroma/bookmark_folders`
+### Gets user bookmark folders
+* Authentication: required
+
+* Response: JSON. Returns a list of bookmark folders.
+* Example response:
+```json
+[
+ {
+ "id": "9umDrYheeY451cQnEe",
+ "name": "Read later",
+ "emoji": "🕓",
+ "source": {
+ "emoji": "🕓"
+ }
+ }
+]
+```
+
+## `POST /api/v1/pleroma/bookmark_folders`
+### Creates a bookmark folder
+* Authentication: required
+
+* Params:
+ * `name`: folder name
+ * `emoji`: folder emoji (optional)
+* Response: JSON. Returns a single bookmark folder.
+
+## `PATCH /api/v1/pleroma/bookmark_folders/:id`
+### Updates a bookmark folder
+* Authentication: required
+
+* Params:
+ * `id`: folder id
+ * `name`: folder name (optional)
+ * `emoji`: folder emoji (optional)
+* Response: JSON. Returns a single bookmark folder.
+
+## `DELETE /api/v1/pleroma/bookmark_folders/:id`
+### Deletes a bookmark folder
+* Authentication: required
+
+* Params:
+ * `id`: folder id
+* Response: JSON. Returns a single bookmark folder.
+
## `/api/v1/pleroma/mascot`
### Gets user mascot image
* Method `GET`
@@ -372,6 +435,15 @@ See [Admin-API](admin_api.md)
* `alias`: the nickname of the alias to delete, e.g. `foo@example.org`.
* Response: JSON. Returns `{"status": "success"}` if the change was successful, `{"error": "[error message]"}` otherwise
+## `/api/v1/pleroma/remote_interaction`
+## Interact with profile or status from remote account
+* Metod `POST`
+* Authentication: not required
+* Params:
+ * `ap_id`: Profile or status ActivityPub ID
+ * `profile`: Remote profile webfinger
+* Response: JSON. Returns `{"url": "[redirect url]"}` on success, `{"error": "[error message]"}` otherwise
+
# Pleroma Conversations
Pleroma Conversations have the same general structure that Mastodon Conversations have. The behavior differs in the following ways when using these endpoints:
@@ -382,7 +454,7 @@ Pleroma Conversations have the same general structure that Mastodon Conversation
Conversations have the additional field `recipients` under the `pleroma` key. This holds a list of all the accounts that will receive a message in this conversation.
-The status posting endpoint takes an additional parameter, `in_reply_to_conversation_id`, which, when set, will set the visiblity to direct and address only the people who are the recipients of that Conversation.
+The status posting endpoint takes an additional parameter, `in_reply_to_conversation_id`, which, when set, will set the visibility to direct and address only the people who are the recipients of that Conversation.
⚠ Conversation IDs can be found in direct messages with the `pleroma.direct_conversation_id` key, do not confuse it with `pleroma.conversation_id`.
diff --git a/docs/development/ap_extensions.md b/docs/development/ap_extensions.md
@@ -20,16 +20,16 @@ Content-Type: multipart/form-data
Parameters:
- (required) `file`: The file being uploaded
-- (optionnal) `description`: A plain-text description of the media, for accessibility purposes.
+- (optional) `description`: A plain-text description of the media, for accessibility purposes.
Response: HTTP 201 Created with the object into the body, no `Location` header provided as it doesn't have an `id`
-The object given in the reponse should then be inserted into an Object's `attachment` field.
+The object given in the response should then be inserted into an Object's `attachment` field.
## ChatMessages
`ChatMessage`s are the messages sent in 1-on-1 chats. They are similar to
-`Note`s, but the addresing is done by having a single AP actor in the `to`
+`Note`s, but the addressing is done by having a single AP actor in the `to`
field. Addressing multiple actors is not allowed. These messages are always
private, there is no public version of them. They are created with a `Create`
activity.
diff --git a/docs/development/setting_up_pleroma_dev.md b/docs/development/setting_up_pleroma_dev.md
@@ -15,7 +15,7 @@ Pleroma requires some adjustments from the defaults for running the instance loc
2. Change the dev.secret.exs
* Change the scheme in `config :pleroma, Pleroma.Web.Endpoint` to http (see examples below)
* If you want to change other settings, you can do that too
-3. You can now start the server `mix phx.server`. Once it's build and started, you can access the instance on `http://<host>:<port>` (e.g.http://localhost:4000 ) and should be able to do everything locally you normaly can.
+3. You can now start the server `mix phx.server`. Once it's build and started, you can access the instance on `http://<host>:<port>` (e.g.http://localhost:4000 ) and should be able to do everything locally you normally can.
Example config to change the scheme to http. Change the port if you want to run on another port.
```elixir
diff --git a/docs/installation/debian_based_jp.md b/docs/installation/debian_based_jp.md
@@ -12,8 +12,8 @@ Note: This article is potentially outdated because at this time we may not have
### 必要なソフトウェア
-- PostgreSQL 9.6以上 (Ubuntu16.04では9.5しか提供されていないので,[](https://www.postgresql.org/download/linux/ubuntu/)こちらから新しいバージョンを入手してください)
-- `postgresql-contrib` 9.6以上 (同上)
+- PostgreSQL 11.0以上 (Ubuntu16.04では9.5しか提供されていないので,[](https://www.postgresql.org/download/linux/ubuntu/)こちらから新しいバージョンを入手してください)
+- `postgresql-contrib` 11.0以上 (同上)
- Elixir 1.13 以上 ([Debianのリポジトリからインストールしないこと!!! ここからインストールすること!](https://elixir-lang.org/install.html#unix-and-unix-like)。または [asdf](https://github.com/asdf-vm/asdf) をpleromaユーザーでインストールしてください)
- `erlang-dev`
- `erlang-nox`
diff --git a/docs/installation/generic_dependencies.include b/docs/installation/generic_dependencies.include
@@ -1,8 +1,8 @@
## Required dependencies
-* PostgreSQL >=9.6
+* PostgreSQL >=11.0
* Elixir >=1.13.0 <1.15
-* Erlang OTP >=22.2.0 <26
+* Erlang OTP >=22.2.0 (supported: <27)
* git
* file / libmagic
* gcc or clang
diff --git a/docs/installation/gentoo_en.md b/docs/installation/gentoo_en.md
@@ -59,7 +59,7 @@ Gentoo quite pointedly does not come with a cron daemon installed, and as such i
If you would not like to install the optional packages, remove them from this line.
-If you're running this from a low-powered virtual machine, it should work though it will take some time. There were no issues on a VPS with a single core and 1GB of RAM; if you are using an even more limited device and run into issues, you can try creating a swapfile or use a more powerful machine running Gentoo to [cross build](https://wiki.gentoo.org/wiki/Cross_build_environment). If you have a wait ahead of you, now would be a good time to take a break, strech a bit, refresh your beverage of choice and/or get a snack, and reply to Arch users' posts with "I use Gentoo btw" as we do.
+If you're running this from a low-powered virtual machine, it should work though it will take some time. There were no issues on a VPS with a single core and 1GB of RAM; if you are using an even more limited device and run into issues, you can try creating a swapfile or use a more powerful machine running Gentoo to [cross build](https://wiki.gentoo.org/wiki/Cross_build_environment). If you have a wait ahead of you, now would be a good time to take a break, stretch a bit, refresh your beverage of choice and/or get a snack, and reply to Arch users' posts with "I use Gentoo btw" as we do.
### Install PostgreSQL
@@ -104,7 +104,7 @@ Not only does this make it much easier to deploy changes you make, as you can co
* Add a new system user for the Pleroma service and set up default directories:
-Remove `,wheel` if you do not want this user to be able to use `sudo`, however note that being able to `sudo` as the `pleroma` user will make finishing the insallation and common maintenence tasks somewhat easier:
+Remove `,wheel` if you do not want this user to be able to use `sudo`, however note that being able to `sudo` as the `pleroma` user will make finishing the installation and common maintenance tasks somewhat easier:
```shell
# useradd -m -G users,wheel -s /bin/bash pleroma
diff --git a/docs/installation/gentoo_otp_en.md b/docs/installation/gentoo_otp_en.md
@@ -49,7 +49,7 @@ Gentoo quite pointedly does not come with a cron daemon installed, and as such i
If you would not like to install the optional packages, remove them from this line.
-If you're running this from a low-powered virtual machine, it should work though it will take some time. There were no issues on a VPS with a single core and 1GB of RAM; if you are using an even more limited device and run into issues, you can try creating a swapfile or use a more powerful machine running Gentoo to [cross build](https://wiki.gentoo.org/wiki/Cross_build_environment). If you have a wait ahead of you, now would be a good time to take a break, strech a bit, refresh your beverage of choice and/or get a snack, and reply to Arch users' posts with "I use Gentoo btw" as we do.
+If you're running this from a low-powered virtual machine, it should work though it will take some time. There were no issues on a VPS with a single core and 1GB of RAM; if you are using an even more limited device and run into issues, you can try creating a swapfile or use a more powerful machine running Gentoo to [cross build](https://wiki.gentoo.org/wiki/Cross_build_environment). If you have a wait ahead of you, now would be a good time to take a break, stretch a bit, refresh your beverage of choice and/or get a snack, and reply to Arch users' posts with "I use Gentoo btw" as we do.
### Setup PostgreSQL
diff --git a/docs/installation/openbsd_en.md b/docs/installation/openbsd_en.md
@@ -62,7 +62,7 @@ rcctl start postgresql
To check that it started properly and didn't fail right after starting, you can run `ps aux | grep postgres`, there should be multiple lines of output.
#### httpd
-httpd will have three fuctions:
+httpd will have three functions:
* redirect requests trying to reach the instance over http to the https URL
* serve a robots.txt file
@@ -225,7 +225,7 @@ pass in quick on $if inet6 proto icmp6 to ($if) icmp6-type { echoreq unreach par
pass in quick on $if proto tcp to ($if) port { http https } # relayd/httpd
pass in quick on $if proto tcp from $authorized_ssh_clients to ($if) port ssh
```
-Replace *<network interface\>* by your server's network interface name (which you can get with ifconfig). Consider replacing the content of the authorized\_ssh\_clients macro by, for exemple, your home IP address, to avoid SSH connection attempts from bots.
+Replace *<network interface\>* by your server's network interface name (which you can get with ifconfig). Consider replacing the content of the authorized\_ssh\_clients macro by, for example, your home IP address, to avoid SSH connection attempts from bots.
Check pf's configuration by running `pfctl -nf /etc/pf.conf`, load it with `pfctl -f /etc/pf.conf` and enable pf at boot with `rcctl enable pf`.
diff --git a/docs/installation/otp_en.md b/docs/installation/otp_en.md
@@ -238,7 +238,7 @@ At this point if you open your (sub)domain in a browser you should see a 502 err
systemctl enable pleroma
```
-If everything worked, you should see Pleroma-FE when visiting your domain. If that didn't happen, try reviewing the installation steps, starting Pleroma in the foreground and seeing if there are any errrors.
+If everything worked, you should see Pleroma-FE when visiting your domain. If that didn't happen, try reviewing the installation steps, starting Pleroma in the foreground and seeing if there are any errors.
Questions about the installation or didn’t it work as it should be, ask in [#pleroma:libera.chat](https://matrix.to/#/#pleroma:libera.chat) via Matrix or **#pleroma** on **libera.chat** via IRC, you can also [file an issue on our Gitlab](https://git.pleroma.social/pleroma/pleroma-support/issues/new).
diff --git a/installation/pleroma-mongooseim.cfg b/installation/pleroma-mongooseim.cfg
@@ -204,7 +204,7 @@
]}
]},
- %% Following HTTP API is deprected, the new one abouve should be used instead
+ %% Following HTTP API is deprecated, the new one above should be used instead
{ {5288, "127.0.0.1"} , ejabberd_cowboy, [
{num_acceptors, 10},
@@ -824,7 +824,7 @@
%% Enable archivization for private messages (default)
% {pm, [
- %% Top-level options can be overriden here if needed, for example:
+ %% Top-level options can be overridden here if needed, for example:
% {async_writer, false}
% ]},
@@ -834,7 +834,7 @@
%%
% {muc, [
% {host, "muc.@HOST@"}
- %% As with pm, top-level options can be overriden for MUC archive
+ %% As with pm, top-level options can be overridden for MUC archive
% ]},
%
%% Do not use a <stanza-id/> element (by default stanzaid is used)
diff --git a/lib/mix/tasks/pleroma/database.ex b/lib/mix/tasks/pleroma/database.ex
@@ -193,7 +193,7 @@ defmodule Mix.Tasks.Pleroma.Database do
"ALTER DATABASE #{db} SET default_text_search_config = '#{tsconfig}';"
)
- # non-exist config will not raise excpetion but only give >0 messages
+ # non-exist config will not raise exception but only give >0 messages
if length(msg) > 0 do
shell_info("Error: #{inspect(msg, pretty: true)}")
else
diff --git a/lib/mix/tasks/pleroma/digest.ex b/lib/mix/tasks/pleroma/digest.ex
@@ -30,7 +30,7 @@ defmodule Mix.Tasks.Pleroma.Digest do
shell_info("Digest email have been sent to #{nickname} (#{user.email})")
else
_ ->
- shell_info("Cound't find any mentions for #{nickname} since #{last_digest_emailed_at}")
+ shell_info("Couldn't find any mentions for #{nickname} since #{last_digest_emailed_at}")
end
end
end
diff --git a/lib/mix/tasks/pleroma/ecto/rollback.ex b/lib/mix/tasks/pleroma/ecto/rollback.ex
@@ -61,7 +61,7 @@ defmodule Mix.Tasks.Pleroma.Ecto.Rollback do
Logger.configure(level: :info)
if opts[:env] == "test" do
- Logger.info("Rollback succesfully")
+ Logger.info("Rollback successfully")
else
{:ok, _, _} =
Ecto.Migrator.with_repo(Pleroma.Repo, &Ecto.Migrator.run(&1, path, :down, opts))
diff --git a/lib/mix/tasks/pleroma/emoji.ex b/lib/mix/tasks/pleroma/emoji.ex
@@ -111,7 +111,7 @@ defmodule Mix.Tasks.Pleroma.Emoji do
{:ok, _} =
:zip.unzip(binary_archive,
- cwd: pack_path,
+ cwd: String.to_charlist(pack_path),
file_list: files_to_unzip
)
diff --git a/lib/mix/tasks/pleroma/instance.ex b/lib/mix/tasks/pleroma/instance.ex
@@ -292,7 +292,7 @@ defmodule Mix.Tasks.Pleroma.Instance do
if db_configurable? do
shell_info(
- " Please transfer your config to the database after running database migrations. Refer to \"Transfering the config to/from the database\" section of the docs for more information."
+ " Please transfer your config to the database after running database migrations. Refer to \"Transferring the config to/from the database\" section of the docs for more information."
)
end
else
@@ -352,6 +352,4 @@ defmodule Mix.Tasks.Pleroma.Instance do
enabled_filters
end
-
- defp upload_filters(_), do: []
end
diff --git a/lib/phoenix/transports/web_socket/raw.ex b/lib/phoenix/transports/web_socket/raw.ex
@@ -1,93 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Phoenix.Transports.WebSocket.Raw do
- import Plug.Conn,
- only: [
- fetch_query_params: 1,
- send_resp: 3
- ]
-
- alias Phoenix.Socket.Transport
-
- def default_config do
- [
- timeout: 60_000,
- transport_log: false,
- cowboy: Phoenix.Endpoint.CowboyWebSocket
- ]
- end
-
- def init(%Plug.Conn{method: "GET"} = conn, {endpoint, handler, transport}) do
- {_, opts} = handler.__transport__(transport)
-
- conn =
- conn
- |> fetch_query_params
- |> Transport.transport_log(opts[:transport_log])
- |> Transport.check_origin(handler, endpoint, opts)
-
- case conn do
- %{halted: false} = conn ->
- case handler.connect(%{
- endpoint: endpoint,
- transport: transport,
- options: [serializer: nil],
- params: conn.params
- }) do
- {:ok, socket} ->
- {:ok, conn, {__MODULE__, {socket, opts}}}
-
- :error ->
- send_resp(conn, :forbidden, "")
- {:error, conn}
- end
-
- _ ->
- {:error, conn}
- end
- end
-
- def init(conn, _) do
- send_resp(conn, :bad_request, "")
- {:error, conn}
- end
-
- def ws_init({socket, config}) do
- Process.flag(:trap_exit, true)
- {:ok, %{socket: socket}, config[:timeout]}
- end
-
- def ws_handle(op, data, state) do
- state.socket.handler
- |> apply(:handle, [op, data, state])
- |> case do
- {op, data} ->
- {:reply, {op, data}, state}
-
- {op, data, state} ->
- {:reply, {op, data}, state}
-
- %{} = state ->
- {:ok, state}
-
- _ ->
- {:ok, state}
- end
- end
-
- def ws_info({_, _} = tuple, state) do
- {:reply, tuple, state}
- end
-
- def ws_info(_tuple, state), do: {:ok, state}
-
- def ws_close(state) do
- ws_handle(:closed, :normal, state)
- end
-
- def ws_terminate(reason, state) do
- ws_handle(:closed, reason, state)
- end
-end
diff --git a/lib/pleroma/activity/html.ex b/lib/pleroma/activity/html.ex
@@ -28,7 +28,7 @@ defmodule Pleroma.Activity.HTML do
end
end
- defp add_cache_key_for(activity_id, additional_key) do
+ def add_cache_key_for(activity_id, additional_key) do
current = get_cache_keys_for(activity_id)
unless additional_key in current do
diff --git a/lib/pleroma/activity/queries.ex b/lib/pleroma/activity/queries.ex
@@ -9,7 +9,7 @@ defmodule Pleroma.Activity.Queries do
import Ecto.Query, only: [from: 2, where: 3]
- @type query :: Ecto.Queryable.t() | Activity.t()
+ @type query :: Ecto.Queryable.t() | Pleroma.Activity.t()
alias Pleroma.Activity
alias Pleroma.User
diff --git a/lib/pleroma/announcement.ex b/lib/pleroma/announcement.ex
@@ -23,19 +23,21 @@ defmodule Pleroma.Announcement do
timestamps(type: :utc_datetime)
end
- def change(struct, params \\ %{}) do
- struct
- |> cast(validate_params(struct, params), [:data, :starts_at, :ends_at, :rendered])
+ @doc "Generates changeset for %Pleroma.Announcement{}"
+ @spec changeset(%__MODULE__{}, map()) :: %Ecto.Changeset{}
+ def changeset(announcement \\ %__MODULE__{}, params \\ %{data: %{}}) do
+ announcement
+ |> cast(validate_params(announcement, params), [:data, :starts_at, :ends_at, :rendered])
|> validate_required([:data])
end
- defp validate_params(struct, params) do
+ defp validate_params(announcement, params) do
base_data =
%{
"content" => "",
"all_day" => false
}
- |> Map.merge((struct && struct.data) || %{})
+ |> Map.merge((announcement && announcement.data) || %{})
merged_data =
Map.merge(base_data, params.data)
@@ -61,13 +63,13 @@ defmodule Pleroma.Announcement do
end
def add(params) do
- changeset = change(%__MODULE__{}, params)
+ changeset = changeset(%__MODULE__{}, params)
Repo.insert(changeset)
end
def update(announcement, params) do
- changeset = change(announcement, params)
+ changeset = changeset(announcement, params)
Repo.update(changeset)
end
diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex
@@ -14,7 +14,6 @@ defmodule Pleroma.Application do
@name Mix.Project.config()[:name]
@version Mix.Project.config()[:version]
@repository Mix.Project.config()[:source_url]
- @mix_env Mix.env()
def name, do: @name
def version, do: @version
@@ -98,7 +97,7 @@ defmodule Pleroma.Application do
{Task.Supervisor, name: Pleroma.TaskSupervisor}
] ++
cachex_children() ++
- http_children(adapter, @mix_env) ++
+ http_children(adapter) ++
[
Pleroma.Stats,
Pleroma.JobQueueMonitor,
@@ -106,8 +105,9 @@ defmodule Pleroma.Application do
{Oban, Config.get(Oban)},
Pleroma.Web.Endpoint
] ++
- task_children(@mix_env) ++
- dont_run_in_test(@mix_env) ++
+ task_children() ++
+ streamer_registry() ++
+ background_migrators() ++
shout_child(shout_enabled?()) ++
[Pleroma.Gopher.Server]
@@ -116,36 +116,10 @@ defmodule Pleroma.Application do
# 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
+ max_restarts = Application.get_env(:pleroma, __MODULE__)[:max_restarts]
opts = [strategy: :one_for_one, name: Pleroma.Supervisor, max_restarts: max_restarts]
- result = Supervisor.start_link(children, opts)
-
- set_postgres_server_version()
-
- result
- end
-
- defp set_postgres_server_version do
- version =
- with %{rows: [[version]]} <- Ecto.Adapters.SQL.query!(Pleroma.Repo, "show server_version"),
- {num, _} <- Float.parse(version) do
- num
- else
- e ->
- Logger.warning(
- "Could not get the postgres version: #{inspect(e)}.\nSetting the default value of 9.6"
- )
-
- 9.6
- end
-
- :persistent_term.put({Pleroma.Repo, :postgres_version}, version)
+ Supervisor.start_link(children, opts)
end
def load_custom_modules do
@@ -159,7 +133,7 @@ defmodule Pleroma.Application do
raise "Invalid custom modules"
{:ok, modules, _warnings} ->
- if @mix_env != :test do
+ if Application.get_env(:pleroma, __MODULE__)[:load_custom_modules] do
Enum.each(modules, fn mod ->
Logger.info("Custom module loaded: #{inspect(mod)}")
end)
@@ -182,6 +156,7 @@ defmodule Pleroma.Application do
build_cachex("web_resp", limit: 2500),
build_cachex("emoji_packs", expiration: emoji_packs_expiration(), limit: 10),
build_cachex("failed_proxy_url", limit: 2500),
+ build_cachex("failed_media_helper_url", default_ttl: :timer.minutes(15), limit: 2_500),
build_cachex("banned_urls", default_ttl: :timer.hours(24 * 30), limit: 5_000),
build_cachex("chat_message_id_idempotency_key",
expiration: chat_message_id_idempotency_key_expiration(),
@@ -213,24 +188,30 @@ defmodule Pleroma.Application do
defp shout_enabled?, do: Config.get([:shout, :enabled])
- defp dont_run_in_test(env) when env in [:test, :benchmark], do: []
-
- defp dont_run_in_test(_) do
- [
- {Registry,
- [
- name: Pleroma.Web.Streamer.registry(),
- keys: :duplicate,
- partitions: System.schedulers_online()
- ]}
- ] ++ background_migrators()
+ defp streamer_registry do
+ if Application.get_env(:pleroma, __MODULE__)[:streamer_registry] do
+ [
+ {Registry,
+ [
+ name: Pleroma.Web.Streamer.registry(),
+ keys: :duplicate,
+ partitions: System.schedulers_online()
+ ]}
+ ]
+ else
+ []
+ end
end
defp background_migrators do
- [
- Pleroma.Migrators.HashtagsTableMigrator,
- Pleroma.Migrators.ContextObjectsDeletionMigrator
- ]
+ if Application.get_env(:pleroma, __MODULE__)[:background_migrators] do
+ [
+ Pleroma.Migrators.HashtagsTableMigrator,
+ Pleroma.Migrators.ContextObjectsDeletionMigrator
+ ]
+ else
+ []
+ end
end
defp shout_child(true) do
@@ -242,37 +223,43 @@ defmodule Pleroma.Application do
defp shout_child(_), do: []
- defp task_children(:test) do
- [
+ defp task_children do
+ children = [
%{
id: :web_push_init,
start: {Task, :start_link, [&Pleroma.Web.Push.init/0]},
restart: :temporary
}
]
- end
- defp task_children(_) do
- [
- %{
- id: :web_push_init,
- start: {Task, :start_link, [&Pleroma.Web.Push.init/0]},
- restart: :temporary
- },
- %{
- id: :internal_fetch_init,
- start: {Task, :start_link, [&Pleroma.Web.ActivityPub.InternalFetchActor.init/0]},
- restart: :temporary
- }
- ]
+ if Application.get_env(:pleroma, __MODULE__)[:internal_fetch] do
+ children ++
+ [
+ %{
+ id: :internal_fetch_init,
+ start: {Task, :start_link, [&Pleroma.Web.ActivityPub.InternalFetchActor.init/0]},
+ restart: :temporary
+ }
+ ]
+ else
+ children
+ end
end
# start hackney and gun pools in tests
- defp http_children(_, :test) do
- http_children(Tesla.Adapter.Hackney, nil) ++ http_children(Tesla.Adapter.Gun, nil)
+ defp http_children(adapter) do
+ if Application.get_env(:pleroma, __MODULE__)[:test_http_pools] do
+ http_children_hackney() ++ http_children_gun()
+ else
+ cond do
+ match?(Tesla.Adapter.Hackney, adapter) -> http_children_hackney()
+ match?(Tesla.Adapter.Gun, adapter) -> http_children_gun()
+ true -> []
+ end
+ end
end
- defp http_children(Tesla.Adapter.Hackney, _) do
+ defp http_children_hackney do
pools = [:federation, :media]
pools =
@@ -288,13 +275,11 @@ defmodule Pleroma.Application do
end
end
- defp http_children(Tesla.Adapter.Gun, _) do
+ defp http_children_gun do
Pleroma.Gun.ConnectionPool.children() ++
[{Task, &Pleroma.HTTP.AdapterHelper.Gun.limiter_setup/0}]
end
- defp http_children(_, _), do: []
-
@spec limiters_setup() :: :ok
def limiters_setup do
config = Config.get(ConcurrentLimiter, [])
diff --git a/lib/pleroma/application_requirements.ex b/lib/pleroma/application_requirements.ex
@@ -7,7 +7,10 @@ defmodule Pleroma.ApplicationRequirements do
The module represents the collection of validations to runs before start server.
"""
- defmodule VerifyError, do: defexception([:message])
+ defmodule VerifyError do
+ defexception([:message])
+ @type t :: %__MODULE__{}
+ end
alias Pleroma.Config
alias Pleroma.Helpers.MediaHelper
@@ -25,6 +28,7 @@ defmodule Pleroma.ApplicationRequirements do
|> check_welcome_message_config!()
|> check_rum!()
|> check_repo_pool_size!()
+ |> check_mrfs()
|> handle_result()
end
@@ -193,8 +197,6 @@ defmodule Pleroma.ApplicationRequirements do
end
end
- defp check_system_commands!(result), do: result
-
defp check_repo_pool_size!(:ok) do
if Pleroma.Config.get([Pleroma.Repo, :pool_size], 10) != 10 and
not Pleroma.Config.get([:dangerzone, :override_repo_pool_size], false) do
@@ -233,4 +235,25 @@ defmodule Pleroma.ApplicationRequirements do
true
end
end
+
+ defp check_mrfs(:ok) do
+ mrfs = Config.get!([:mrf, :policies])
+
+ missing_mrfs =
+ Enum.reduce(mrfs, [], fn x, acc ->
+ if Code.ensure_compiled(x) do
+ acc
+ else
+ acc ++ [x]
+ end
+ end)
+
+ if Enum.empty?(missing_mrfs) do
+ :ok
+ else
+ {:error, "The following MRF modules are configured but missing: #{inspect(missing_mrfs)}"}
+ end
+ end
+
+ defp check_mrfs(result), do: result
end
diff --git a/lib/pleroma/bookmark.ex b/lib/pleroma/bookmark.ex
@@ -10,6 +10,7 @@ defmodule Pleroma.Bookmark do
alias Pleroma.Activity
alias Pleroma.Bookmark
+ alias Pleroma.BookmarkFolder
alias Pleroma.Repo
alias Pleroma.User
@@ -18,33 +19,46 @@ defmodule Pleroma.Bookmark do
schema "bookmarks" do
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
belongs_to(:activity, Activity, type: FlakeId.Ecto.CompatType)
+ belongs_to(:folder, BookmarkFolder, type: FlakeId.Ecto.CompatType)
timestamps()
end
- @spec create(FlakeId.Ecto.CompatType.t(), FlakeId.Ecto.CompatType.t()) ::
- {:ok, Bookmark.t()} | {:error, Changeset.t()}
- def create(user_id, activity_id) do
+ @spec create(Ecto.UUID.t(), Ecto.UUID.t()) ::
+ {:ok, Bookmark.t()} | {:error, Ecto.Changeset.t()}
+ def create(user_id, activity_id, folder_id \\ nil) do
attrs = %{
user_id: user_id,
- activity_id: activity_id
+ activity_id: activity_id,
+ folder_id: folder_id
}
%Bookmark{}
- |> cast(attrs, [:user_id, :activity_id])
+ |> cast(attrs, [:user_id, :activity_id, :folder_id])
|> validate_required([:user_id, :activity_id])
|> unique_constraint(:activity_id, name: :bookmarks_user_id_activity_id_index)
- |> Repo.insert()
+ |> Repo.insert(
+ on_conflict: [set: [folder_id: folder_id]],
+ conflict_target: [:user_id, :activity_id]
+ )
end
- @spec for_user_query(FlakeId.Ecto.CompatType.t()) :: Ecto.Query.t()
- def for_user_query(user_id) do
+ @spec for_user_query(Ecto.UUID.t()) :: Ecto.Query.t()
+ def for_user_query(user_id, folder_id \\ nil) do
Bookmark
|> where(user_id: ^user_id)
+ |> maybe_filter_by_folder(folder_id)
|> join(:inner, [b], activity in assoc(b, :activity))
|> preload([b, a], activity: a)
end
+ defp maybe_filter_by_folder(query, nil), do: query
+
+ defp maybe_filter_by_folder(query, folder_id) do
+ query
+ |> where(folder_id: ^folder_id)
+ end
+
def get(user_id, activity_id) do
Bookmark
|> where(user_id: ^user_id)
@@ -52,8 +66,8 @@ defmodule Pleroma.Bookmark do
|> Repo.one()
end
- @spec destroy(FlakeId.Ecto.CompatType.t(), FlakeId.Ecto.CompatType.t()) ::
- {:ok, Bookmark.t()} | {:error, Changeset.t()}
+ @spec destroy(Ecto.UUID.t(), Ecto.UUID.t()) ::
+ {:ok, Bookmark.t()} | {:error, Ecto.Changeset.t()}
def destroy(user_id, activity_id) do
from(b in Bookmark,
where: b.user_id == ^user_id,
@@ -62,4 +76,11 @@ defmodule Pleroma.Bookmark do
|> Repo.one()
|> Repo.delete()
end
+
+ def set_folder(bookmark, folder_id) do
+ bookmark
+ |> cast(%{folder_id: folder_id}, [:folder_id])
+ |> validate_required([:folder_id])
+ |> Repo.update()
+ end
end
diff --git a/lib/pleroma/bookmark_folder.ex b/lib/pleroma/bookmark_folder.ex
@@ -0,0 +1,115 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.BookmarkFolder do
+ use Ecto.Schema
+
+ import Ecto.Changeset
+ import Ecto.Query
+
+ alias Pleroma.BookmarkFolder
+ alias Pleroma.Emoji
+ alias Pleroma.Repo
+ alias Pleroma.User
+
+ @type t :: %__MODULE__{}
+ @primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
+
+ schema "bookmark_folders" do
+ field(:name, :string)
+ field(:emoji, :string)
+
+ belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
+
+ timestamps()
+ end
+
+ def get_by_id(id), do: Repo.get_by(BookmarkFolder, id: id)
+
+ def create(user_id, name, emoji \\ nil) do
+ %BookmarkFolder{}
+ |> cast(
+ %{
+ user_id: user_id,
+ name: name,
+ emoji: emoji
+ },
+ [:user_id, :name, :emoji]
+ )
+ |> validate_required([:user_id, :name])
+ |> fix_emoji()
+ |> validate_emoji()
+ |> unique_constraint([:user_id, :name])
+ |> Repo.insert()
+ end
+
+ def update(folder_id, name, emoji \\ nil) do
+ get_by_id(folder_id)
+ |> cast(
+ %{
+ name: name,
+ emoji: emoji
+ },
+ [:name, :emoji]
+ )
+ |> fix_emoji()
+ |> validate_emoji()
+ |> unique_constraint([:user_id, :name])
+ |> Repo.update()
+ end
+
+ defp fix_emoji(changeset) do
+ with {:emoji_field, emoji} when is_binary(emoji) <-
+ {:emoji_field, get_field(changeset, :emoji)},
+ {:fixed_emoji, emoji} <-
+ {:fixed_emoji,
+ emoji
+ |> Pleroma.Emoji.fully_qualify_emoji()
+ |> Pleroma.Emoji.maybe_quote()} do
+ put_change(changeset, :emoji, emoji)
+ else
+ {:emoji_field, _} -> changeset
+ end
+ end
+
+ defp validate_emoji(changeset) do
+ validate_change(changeset, :emoji, fn
+ :emoji, nil ->
+ []
+
+ :emoji, emoji ->
+ if Emoji.unicode?(emoji) or valid_local_custom_emoji?(emoji) do
+ []
+ else
+ [emoji: "Invalid emoji"]
+ end
+ end)
+ end
+
+ defp valid_local_custom_emoji?(emoji) do
+ with %{file: _path} <- Emoji.get(emoji) do
+ true
+ else
+ _ -> false
+ end
+ end
+
+ def delete(folder_id) do
+ BookmarkFolder
+ |> Repo.get_by(id: folder_id)
+ |> Repo.delete()
+ end
+
+ def for_user(user_id) do
+ BookmarkFolder
+ |> where(user_id: ^user_id)
+ |> Repo.all()
+ end
+
+ def belongs_to_user?(folder_id, user_id) do
+ BookmarkFolder
+ |> where(id: ^folder_id, user_id: ^user_id)
+ |> Repo.exists?()
+ end
+end
diff --git a/lib/pleroma/caching.ex b/lib/pleroma/caching.ex
@@ -8,10 +8,13 @@ defmodule Pleroma.Caching do
@callback put(Cachex.cache(), any(), any(), Keyword.t()) :: {Cachex.status(), boolean()}
@callback put(Cachex.cache(), any(), any()) :: {Cachex.status(), boolean()}
@callback fetch!(Cachex.cache(), any(), function() | nil) :: any()
+ @callback fetch(Cachex.cache(), any(), function() | nil) ::
+ {atom(), any()} | {atom(), any(), any()}
# @callback del(Cachex.cache(), any(), Keyword.t()) :: {Cachex.status(), boolean()}
@callback del(Cachex.cache(), any()) :: {Cachex.status(), boolean()}
@callback stream!(Cachex.cache(), any()) :: Enumerable.t()
@callback expire_at(Cachex.cache(), binary(), number()) :: {Cachex.status(), boolean()}
+ @callback expire(Cachex.cache(), binary(), number()) :: {Cachex.status(), boolean()}
@callback exists?(Cachex.cache(), any()) :: {Cachex.status(), boolean()}
@callback execute!(Cachex.cache(), function()) :: any()
@callback get_and_update(Cachex.cache(), any(), function()) ::
diff --git a/lib/pleroma/captcha/kocaptcha.ex b/lib/pleroma/captcha/kocaptcha.ex
@@ -29,7 +29,7 @@ defmodule Pleroma.Captcha.Kocaptcha do
@impl Service
def validate(_token, captcha, answer_data) do
- # Here the token is unsed, because the unencrypted captcha answer is just passed to method
+ # Here the token is unused, because the unencrypted captcha answer is just passed to method
if not is_nil(captcha) and
:crypto.hash(:md5, captcha) |> Base.encode16() == String.upcase(answer_data),
do: :ok,
diff --git a/lib/pleroma/chat.ex b/lib/pleroma/chat.ex
@@ -42,7 +42,7 @@ defmodule Pleroma.Chat do
|> unique_constraint(:user_id, name: :chats_user_id_recipient_index)
end
- @spec get_by_user_and_id(User.t(), FlakeId.Ecto.CompatType.t()) ::
+ @spec get_by_user_and_id(User.t(), Ecto.UUID.t()) ::
{:ok, t()} | {:error, :not_found}
def get_by_user_and_id(%User{id: user_id}, id) do
from(c in __MODULE__,
@@ -52,17 +52,17 @@ defmodule Pleroma.Chat do
|> Repo.find_resource()
end
- @spec get_by_id(FlakeId.Ecto.CompatType.t()) :: t() | nil
+ @spec get_by_id(Ecto.UUID.t()) :: t() | nil
def get_by_id(id) do
Repo.get(__MODULE__, id)
end
- @spec get(FlakeId.Ecto.CompatType.t(), String.t()) :: t() | nil
+ @spec get(Ecto.UUID.t(), String.t()) :: t() | nil
def get(user_id, recipient) do
Repo.get_by(__MODULE__, user_id: user_id, recipient: recipient)
end
- @spec get_or_create(FlakeId.Ecto.CompatType.t(), String.t()) ::
+ @spec get_or_create(Ecto.UUID.t(), String.t()) ::
{:ok, t()} | {:error, Ecto.Changeset.t()}
def get_or_create(user_id, recipient) do
%__MODULE__{}
@@ -75,7 +75,7 @@ defmodule Pleroma.Chat do
)
end
- @spec bump_or_create(FlakeId.Ecto.CompatType.t(), String.t()) ::
+ @spec bump_or_create(Ecto.UUID.t(), String.t()) ::
{:ok, t()} | {:error, Ecto.Changeset.t()}
def bump_or_create(user_id, recipient) do
%__MODULE__{}
@@ -87,7 +87,7 @@ defmodule Pleroma.Chat do
)
end
- @spec for_user_query(FlakeId.Ecto.CompatType.t()) :: Ecto.Query.t()
+ @spec for_user_query(Ecto.UUID.t()) :: Ecto.Query.t()
def for_user_query(user_id) do
from(c in Chat,
where: c.user_id == ^user_id,
diff --git a/lib/pleroma/config/deprecation_warnings.ex b/lib/pleroma/config/deprecation_warnings.ex
@@ -172,7 +172,7 @@ defmodule Pleroma.Config.DeprecationWarnings do
```
config :pleroma, :mrf,
- transparency_exclusions: [{"instance.tld", "Reason to exlude transparency"}]
+ transparency_exclusions: [{"instance.tld", "Reason to exclude transparency"}]
```
""")
@@ -213,7 +213,7 @@ defmodule Pleroma.Config.DeprecationWarnings do
check_gun_pool_options(),
check_activity_expiration_config(),
check_remote_ip_plug_name(),
- check_uploders_s3_public_endpoint(),
+ check_uploaders_s3_public_endpoint(),
check_old_chat_shoutbox(),
check_quarantined_instances_tuples(),
check_transparency_exclusions_tuples(),
@@ -256,7 +256,7 @@ defmodule Pleroma.Config.DeprecationWarnings do
move_namespace_and_warn(@mrf_config_map, warning_preface)
end
- @spec move_namespace_and_warn([config_map()], String.t()) :: :ok | nil
+ @spec move_namespace_and_warn([config_map()], String.t()) :: :ok | :error
def move_namespace_and_warn(config_map, warning_preface) do
warning =
Enum.reduce(config_map, "", fn
@@ -279,7 +279,7 @@ defmodule Pleroma.Config.DeprecationWarnings do
end
end
- @spec check_media_proxy_whitelist_config() :: :ok | nil
+ @spec check_media_proxy_whitelist_config() :: :ok | :error
def check_media_proxy_whitelist_config do
whitelist = Config.get([:media_proxy, :whitelist])
@@ -340,7 +340,7 @@ defmodule Pleroma.Config.DeprecationWarnings do
end
end
- @spec check_activity_expiration_config() :: :ok | nil
+ @spec check_activity_expiration_config() :: :ok | :error
def check_activity_expiration_config do
warning_preface = """
!!!DEPRECATION WARNING!!!
@@ -356,7 +356,7 @@ defmodule Pleroma.Config.DeprecationWarnings do
)
end
- @spec check_remote_ip_plug_name() :: :ok | nil
+ @spec check_remote_ip_plug_name() :: :ok | :error
def check_remote_ip_plug_name do
warning_preface = """
!!!DEPRECATION WARNING!!!
@@ -372,8 +372,8 @@ defmodule Pleroma.Config.DeprecationWarnings do
)
end
- @spec check_uploders_s3_public_endpoint() :: :ok | nil
- def check_uploders_s3_public_endpoint do
+ @spec check_uploaders_s3_public_endpoint() :: :ok | :error
+ def check_uploaders_s3_public_endpoint do
s3_config = Pleroma.Config.get([Pleroma.Uploaders.S3])
use_old_config = Keyword.has_key?(s3_config, :public_endpoint)
@@ -393,7 +393,7 @@ defmodule Pleroma.Config.DeprecationWarnings do
end
end
- @spec check_old_chat_shoutbox() :: :ok | nil
+ @spec check_old_chat_shoutbox() :: :ok | :error
def check_old_chat_shoutbox do
instance_config = Pleroma.Config.get([:instance])
chat_config = Pleroma.Config.get([:chat]) || []
diff --git a/lib/pleroma/config/release_runtime_provider.ex b/lib/pleroma/config/release_runtime_provider.ex
@@ -21,7 +21,7 @@ defmodule Pleroma.Config.ReleaseRuntimeProvider do
with_runtime_config =
if File.exists?(config_path) do
# <https://git.pleroma.social/pleroma/pleroma/-/issues/3135>
- %File.Stat{mode: mode} = File.lstat!(config_path)
+ %File.Stat{mode: mode} = File.stat!(config_path)
if Bitwise.band(mode, 0o007) > 0 do
raise "Configuration at #{config_path} has world-permissions, execute the following: chmod o= #{config_path}"
diff --git a/lib/pleroma/config_db.ex b/lib/pleroma/config_db.ex
@@ -54,7 +54,7 @@ defmodule Pleroma.ConfigDB do
@spec get_by_params(map()) :: ConfigDB.t() | nil
def get_by_params(%{group: _, key: _} = params), do: Repo.get_by(ConfigDB, params)
- @spec changeset(ConfigDB.t(), map()) :: Changeset.t()
+ @spec changeset(ConfigDB.t(), map()) :: Ecto.Changeset.t()
def changeset(config, params \\ %{}) do
config
|> cast(params, [:key, :group, :value])
@@ -138,7 +138,7 @@ defmodule Pleroma.ConfigDB do
end
end
- @spec update_or_create(map()) :: {:ok, ConfigDB.t()} | {:error, Changeset.t()}
+ @spec update_or_create(map()) :: {:ok, ConfigDB.t()} | {:error, Ecto.Changeset.t()}
def update_or_create(params) do
params = Map.put(params, :value, to_elixir_types(params[:value]))
search_opts = Map.take(params, [:group, :key])
@@ -175,7 +175,7 @@ defmodule Pleroma.ConfigDB do
end)
end
- @spec delete(ConfigDB.t() | map()) :: {:ok, ConfigDB.t()} | {:error, Changeset.t()}
+ @spec delete(ConfigDB.t() | map()) :: {:ok, ConfigDB.t()} | {:error, Ecto.Changeset.t()}
def delete(%ConfigDB{} = config), do: Repo.delete(config)
def delete(params) do
diff --git a/lib/pleroma/constants.ex b/lib/pleroma/constants.ex
@@ -19,7 +19,8 @@ defmodule Pleroma.Constants do
"context_id",
"deleted_activity_id",
"pleroma_internal",
- "generator"
+ "generator",
+ "rules"
]
)
@@ -76,6 +77,14 @@ defmodule Pleroma.Constants do
]
)
+ const(allowed_user_actor_types,
+ do: [
+ "Person",
+ "Service",
+ "Group"
+ ]
+ )
+
# basic regex, just there to weed out potential mistakes
# https://datatracker.ietf.org/doc/html/rfc2045#section-5.1
const(mime_regex,
diff --git a/lib/pleroma/conversation.ex b/lib/pleroma/conversation.ex
@@ -57,7 +57,7 @@ defmodule Pleroma.Conversation do
3. Bump all relevant participations to 'unread'
"""
def create_or_bump_for(activity, opts \\ []) do
- with true <- Pleroma.Web.ActivityPub.Visibility.is_direct?(activity),
+ with true <- Pleroma.Web.ActivityPub.Visibility.direct?(activity),
"Create" <- activity.data["type"],
%Object{} = object <- Object.normalize(activity, fetch: false),
true <- object.data["type"] in ["Note", "Question"],
diff --git a/lib/pleroma/data_migration.ex b/lib/pleroma/data_migration.ex
@@ -12,6 +12,8 @@ defmodule Pleroma.DataMigration do
import Ecto.Changeset
import Ecto.Query
+ @type t :: %__MODULE__{}
+
schema "data_migrations" do
field(:name, :string)
field(:state, State, default: :pending)
diff --git a/lib/pleroma/docs/generator.ex b/lib/pleroma/docs/generator.ex
@@ -15,7 +15,7 @@ defmodule Pleroma.Docs.Generator do
:code.all_loaded()
|> Enum.filter(fn {module, _} ->
# This shouldn't be needed as all modules are expected to have module_info/1,
- # but in test enviroments some transient modules `:elixir_compiler_XX`
+ # but in test environments some transient modules `:elixir_compiler_XX`
# are loaded for some reason (where XX is a random integer).
Code.ensure_loaded(module)
diff --git a/lib/pleroma/docs/json.ex b/lib/pleroma/docs/json.ex
@@ -18,7 +18,7 @@ defmodule Pleroma.Docs.JSON do
:persistent_term.put(@term, Pleroma.Docs.Generator.convert_to_strings(descriptions))
end
- @spec compiled_descriptions :: Map.t()
+ @spec compiled_descriptions :: map()
def compiled_descriptions do
:persistent_term.get(@term)
end
diff --git a/lib/pleroma/ecto_type/activity_pub/object_validators/bare_uri.ex b/lib/pleroma/ecto_type/activity_pub/object_validators/bare_uri.ex
@@ -8,10 +8,12 @@ defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.BareUri do
def type, do: :string
def cast(uri) when is_binary(uri) do
- case URI.parse(uri) do
- %URI{scheme: nil} -> :error
- %URI{} -> {:ok, uri}
- _ -> :error
+ parsed = URI.parse(uri)
+
+ if is_nil(parsed.scheme) do
+ :error
+ else
+ {:ok, uri}
end
end
diff --git a/lib/pleroma/emoji.ex b/lib/pleroma/emoji.ex
@@ -24,6 +24,8 @@ defmodule Pleroma.Emoji do
defstruct [:code, :file, :tags, :safe_code, :safe_file]
+ @type t :: %__MODULE__{}
+
@doc "Build emoji struct"
def build({code, file, tags}) do
%__MODULE__{
@@ -49,12 +51,12 @@ defmodule Pleroma.Emoji do
end
@doc "Returns the path of the emoji `name`."
- @spec get(String.t()) :: String.t() | nil
+ @spec get(String.t()) :: Pleroma.Emoji.t() | nil
def get(name) do
name = maybe_strip_name(name)
case :ets.lookup(@ets, name) do
- [{_, path}] -> path
+ [{_, emoji}] -> emoji
_ -> nil
end
end
@@ -136,23 +138,23 @@ defmodule Pleroma.Emoji do
emojis = emojis ++ regional_indicators
for emoji <- emojis do
- def is_unicode_emoji?(unquote(emoji)), do: true
+ def unicode?(unquote(emoji)), do: true
end
- def is_unicode_emoji?(_), do: false
+ def unicode?(_), do: false
@emoji_regex ~r/:[A-Za-z0-9_-]+(@.+)?:/
- def is_custom_emoji?(s) when is_binary(s), do: Regex.match?(@emoji_regex, s)
+ def custom?(s) when is_binary(s), do: Regex.match?(@emoji_regex, s)
- def is_custom_emoji?(_), do: false
+ def custom?(_), do: false
def maybe_strip_name(name) when is_binary(name), do: String.trim(name, ":")
def maybe_strip_name(name), do: name
def maybe_quote(name) when is_binary(name) do
- if is_unicode_emoji?(name) do
+ if unicode?(name) do
name
else
if String.starts_with?(name, ":") do
diff --git a/lib/pleroma/emoji/loader.ex b/lib/pleroma/emoji/loader.ex
@@ -15,8 +15,6 @@ defmodule Pleroma.Emoji.Loader do
require Logger
- @mix_env Mix.env()
-
@type pattern :: Regex.t() | module() | String.t()
@type patterns :: pattern() | [pattern()]
@type group_patterns :: keyword(patterns())
@@ -79,7 +77,7 @@ defmodule Pleroma.Emoji.Loader do
# for testing emoji.txt entries we do not want exposed in normal operation
test_emoji =
- if @mix_env == :test do
+ if Application.get_env(:pleroma, __MODULE__)[:test_emoji] do
load_from_file("test/config/emoji.txt", emoji_groups)
else
[]
diff --git a/lib/pleroma/emoji/pack.ex b/lib/pleroma/emoji/pack.ex
@@ -100,7 +100,7 @@ defmodule Pleroma.Emoji.Pack do
{:ok, _emoji_files} =
:zip.unzip(
to_charlist(file.path),
- [{:file_list, Enum.map(emojies, & &1[:path])}, {:cwd, tmp_dir}]
+ [{:file_list, Enum.map(emojies, & &1[:path])}, {:cwd, String.to_charlist(tmp_dir)}]
)
{_, updated_pack} =
@@ -209,7 +209,9 @@ defmodule Pleroma.Emoji.Pack do
with :ok <- validate_shareable_packs_available(uri) do
uri
- |> URI.merge("/api/pleroma/emoji/packs?page=#{opts[:page]}&page_size=#{opts[:page_size]}")
+ |> URI.merge(
+ "/api/v1/pleroma/emoji/packs?page=#{opts[:page]}&page_size=#{opts[:page_size]}"
+ )
|> http_get()
end
end
@@ -249,8 +251,12 @@ defmodule Pleroma.Emoji.Pack do
uri = url |> String.trim() |> URI.parse()
with :ok <- validate_shareable_packs_available(uri),
+ {:ok, %{"files_count" => files_count}} <-
+ uri |> URI.merge("/api/v1/pleroma/emoji/pack?name=#{name}&page_size=0") |> http_get(),
{:ok, remote_pack} <-
- uri |> URI.merge("/api/pleroma/emoji/pack?name=#{name}") |> http_get(),
+ uri
+ |> URI.merge("/api/v1/pleroma/emoji/pack?name=#{name}&page_size=#{files_count}")
+ |> http_get(),
{:ok, %{sha: sha, url: url} = pack_info} <- fetch_pack_info(remote_pack, uri, name),
{:ok, archive} <- download_archive(url, sha),
pack <- copy_as(remote_pack, as || name),
@@ -592,7 +598,7 @@ defmodule Pleroma.Emoji.Pack do
{:ok,
%{
sha: sha,
- url: URI.merge(uri, "/api/pleroma/emoji/packs/archive?name=#{name}") |> to_string()
+ url: URI.merge(uri, "/api/v1/pleroma/emoji/packs/archive?name=#{name}") |> to_string()
}}
%{"fallback-src" => src, "fallback-src-sha256" => sha} when is_binary(src) ->
diff --git a/lib/pleroma/filter.ex b/lib/pleroma/filter.ex
@@ -216,9 +216,6 @@ defmodule Pleroma.Filter do
:re ->
~r/\b#{phrases}\b/i
-
- _ ->
- nil
end
end
diff --git a/lib/pleroma/following_relationship.ex b/lib/pleroma/following_relationship.ex
@@ -241,13 +241,13 @@ defmodule Pleroma.FollowingRelationship do
end
@doc """
- For a query with joined activity,
- keeps rows where activity's actor is followed by user -or- is NOT domain-blocked by user.
+ For a query with joined activity's actor,
+ keeps rows where actor is followed by user -or- is NOT domain-blocked by user.
"""
def keep_following_or_not_domain_blocked(query, user) do
where(
query,
- [_, activity],
+ [_, user_actor: user_actor],
fragment(
# "(actor's domain NOT in domain_blocks) OR (actor IS in followed AP IDs)"
"""
@@ -255,9 +255,9 @@ defmodule Pleroma.FollowingRelationship do
? = ANY(SELECT ap_id FROM users AS u INNER JOIN following_relationships AS fr
ON u.id = fr.following_id WHERE fr.follower_id = ? AND fr.state = ?)
""",
- activity.actor,
+ user_actor.ap_id,
^user.domain_blocks,
- activity.actor,
+ user_actor.ap_id,
^User.binary_id(user.id),
^accept_state_code()
)
diff --git a/lib/pleroma/gopher/server.ex b/lib/pleroma/gopher/server.ex
@@ -114,7 +114,7 @@ defmodule Pleroma.Gopher.Server.ProtocolHandler do
def response("/notices/" <> id) do
with %Activity{} = activity <- Activity.get_by_id(id),
- true <- Visibility.is_public?(activity) do
+ true <- Visibility.public?(activity) do
activities =
ActivityPub.fetch_activities_for_context(activity.data["context"])
|> render_activities
diff --git a/lib/pleroma/gun/connection_pool/reclaimer.ex b/lib/pleroma/gun/connection_pool/reclaimer.ex
@@ -9,7 +9,7 @@ defmodule Pleroma.Gun.ConnectionPool.Reclaimer do
def start_monitor do
pid =
- case :gen_server.start(__MODULE__, [], name: {:via, Registry, {registry(), "reclaimer"}}) do
+ case GenServer.start_link(__MODULE__, [], name: {:via, Registry, {registry(), "reclaimer"}}) do
{:ok, pid} ->
pid
diff --git a/lib/pleroma/gun/connection_pool/worker_supervisor.ex b/lib/pleroma/gun/connection_pool/worker_supervisor.ex
@@ -18,10 +18,12 @@ defmodule Pleroma.Gun.ConnectionPool.WorkerSupervisor do
)
end
- def start_worker(opts, retry \\ false) do
+ def start_worker(opts, last_attempt \\ false) do
case DynamicSupervisor.start_child(__MODULE__, {Pleroma.Gun.ConnectionPool.Worker, opts}) do
{:error, :max_children} ->
- if retry or free_pool() == :error do
+ funs = [fn -> last_attempt end, fn -> match?(:error, free_pool()) end]
+
+ if Enum.any?(funs, fn fun -> fun.() end) do
:telemetry.execute([:pleroma, :connection_pool, :provision_failure], %{opts: opts})
{:error, :pool_full}
else
diff --git a/lib/pleroma/helpers/media_helper.ex b/lib/pleroma/helpers/media_helper.ex
@@ -12,6 +12,8 @@ defmodule Pleroma.Helpers.MediaHelper do
require Logger
+ @cachex Pleroma.Config.get([:cachex, :provider], Cachex)
+
def missing_dependencies do
Enum.reduce([ffmpeg: "ffmpeg"], [], fn {sym, executable}, acc ->
if Pleroma.Utils.command_available?(executable) do
@@ -40,92 +42,46 @@ defmodule Pleroma.Helpers.MediaHelper do
end
# Note: video thumbnail is intentionally not resized (always has original dimensions)
+ @spec video_framegrab(String.t()) :: {:ok, binary()} | {:error, any()}
def video_framegrab(url) do
with executable when is_binary(executable) <- System.find_executable("ffmpeg"),
+ false <- @cachex.exists?(:failed_media_helper_cache, url),
{:ok, env} <- HTTP.get(url, [], pool: :media),
- {:ok, fifo_path} <- mkfifo(),
- args = [
- "-y",
- "-i",
- fifo_path,
- "-vframes",
- "1",
- "-f",
- "mjpeg",
- "-loglevel",
- "error",
- "-"
- ] do
- run_fifo(fifo_path, env, executable, args)
+ {:ok, pid} <- StringIO.open(env.body) do
+ body_stream = IO.binstream(pid, 1)
+
+ task =
+ Task.async(fn ->
+ Exile.stream!(
+ [
+ executable,
+ "-i",
+ "pipe:0",
+ "-vframes",
+ "1",
+ "-f",
+ "mjpeg",
+ "pipe:1"
+ ],
+ input: body_stream,
+ ignore_epipe: true,
+ stderr: :disable
+ )
+ |> Enum.into(<<>>)
+ end)
+
+ case Task.yield(task, 5_000) do
+ nil ->
+ Task.shutdown(task)
+ @cachex.put(:failed_media_helper_cache, url, nil)
+ {:error, {:ffmpeg, :timeout}}
+
+ result ->
+ {:ok, result}
+ end
else
nil -> {:error, {:ffmpeg, :command_not_found}}
{:error, _} = error -> error
end
end
-
- defp run_fifo(fifo_path, env, executable, args) do
- pid =
- Port.open({:spawn_executable, executable}, [
- :use_stdio,
- :stream,
- :exit_status,
- :binary,
- args: args
- ])
-
- fifo = Port.open(to_charlist(fifo_path), [:eof, :binary, :stream, :out])
- fix = Pleroma.Helpers.QtFastStart.fix(env.body)
- true = Port.command(fifo, fix)
- :erlang.port_close(fifo)
- loop_recv(pid)
- after
- File.rm(fifo_path)
- end
-
- defp mkfifo do
- path = Path.join(System.tmp_dir!(), "pleroma-media-preview-pipe-#{Ecto.UUID.generate()}")
-
- case System.cmd("mkfifo", [path]) do
- {_, 0} ->
- spawn(fifo_guard(path))
- {:ok, path}
-
- {_, err} ->
- {:error, {:fifo_failed, err}}
- end
- end
-
- defp fifo_guard(path) do
- pid = self()
-
- fn ->
- ref = Process.monitor(pid)
-
- receive do
- {:DOWN, ^ref, :process, ^pid, _} ->
- File.rm(path)
- end
- end
- end
-
- defp loop_recv(pid) do
- loop_recv(pid, <<>>)
- end
-
- defp loop_recv(pid, acc) do
- receive do
- {^pid, {:data, data}} ->
- loop_recv(pid, acc <> data)
-
- {^pid, {:exit_status, 0}} ->
- {:ok, acc}
-
- {^pid, {:exit_status, status}} ->
- {:error, status}
- after
- 5000 ->
- :erlang.port_close(pid)
- {:error, :timeout}
- end
- end
end
diff --git a/lib/pleroma/helpers/qt_fast_start.ex b/lib/pleroma/helpers/qt_fast_start.ex
@@ -40,16 +40,21 @@ defmodule Pleroma.Helpers.QtFastStart do
got_mdat,
acc
) do
- full_size = (size - 8) * 8
- <<data::bits-size(full_size), rest::bits>> = rest
-
- acc = [
- {fourcc, pos, pos + size, size,
- <<size::integer-big-size(32), fourcc::bits-size(32), data::bits>>}
- | acc
- ]
-
- fix(rest, pos + size, got_moov || fourcc == "moov", got_mdat || fourcc == "mdat", acc)
+ try do
+ full_size = (size - 8) * 8
+ <<data::bits-size(full_size), rest::bits>> = rest
+
+ acc = [
+ {fourcc, pos, pos + size, size,
+ <<size::integer-big-size(32), fourcc::bits-size(32), data::bits>>}
+ | acc
+ ]
+
+ fix(rest, pos + size, got_moov || fourcc == "moov", got_mdat || fourcc == "mdat", acc)
+ rescue
+ _ ->
+ :abort
+ end
end
defp fix(<<>>, _pos, _, _, acc) do
@@ -121,9 +126,15 @@ defmodule Pleroma.Helpers.QtFastStart do
<<pos::integer-big-size(unquote(size)), rest::bits>>,
acc
) do
- rewrite_entries(unquote(size), offset, rest, [
- acc | <<pos + offset::integer-big-size(unquote(size))>>
- ])
+ rewrite_entries(
+ unquote(size),
+ offset,
+ rest,
+ acc ++
+ [
+ <<pos + offset::integer-big-size(unquote(size))>>
+ ]
+ )
end
end
diff --git a/lib/pleroma/html.ex b/lib/pleroma/html.ex
@@ -6,8 +6,6 @@ defmodule Pleroma.HTML do
# Scrubbers are compiled on boot so they can be configured in OTP releases
# @on_load :compile_scrubbers
- @cachex Pleroma.Config.get([:cachex, :provider], Cachex)
-
def compile_scrubbers do
dir = Path.join(:code.priv_dir(:pleroma), "scrubbers")
@@ -67,22 +65,9 @@ defmodule Pleroma.HTML do
end
end
- def extract_first_external_url_from_object(%{data: %{"content" => content}} = object)
+ @spec extract_first_external_url_from_object(Pleroma.Object.t()) :: String.t() | nil
+ def extract_first_external_url_from_object(%{data: %{"content" => content}})
when is_binary(content) do
- unless object.data["fake"] do
- key = "URL|#{object.id}"
-
- @cachex.fetch!(:scrubber_cache, key, fn _key ->
- {:commit, {:ok, extract_first_external_url(content)}}
- end)
- else
- {:ok, extract_first_external_url(content)}
- end
- end
-
- def extract_first_external_url_from_object(_), do: {:error, :no_content}
-
- def extract_first_external_url(content) do
content
|> Floki.parse_fragment!()
|> Floki.find("a:not(.mention,.hashtag,.attachment,[rel~=\"tag\"])")
@@ -90,4 +75,6 @@ defmodule Pleroma.HTML do
|> Floki.attribute("href")
|> Enum.at(0)
end
+
+ def extract_first_external_url_from_object(_), do: nil
end
diff --git a/lib/pleroma/http.ex b/lib/pleroma/http.ex
@@ -106,6 +106,10 @@ defmodule Pleroma.HTTP do
[Tesla.Middleware.FollowRedirects, Pleroma.Tesla.Middleware.ConnectionPool]
end
+ defp adapter_middlewares({Tesla.Adapter.Finch, _}) do
+ [Tesla.Middleware.FollowRedirects]
+ end
+
defp adapter_middlewares(_) do
if Pleroma.Config.get(:env) == :test do
# Emulate redirects in test env, which are handled by adapters in other environments
diff --git a/lib/pleroma/http/adapter_helper.ex b/lib/pleroma/http/adapter_helper.ex
@@ -15,8 +15,8 @@ defmodule Pleroma.HTTP.AdapterHelper do
require Logger
@type proxy ::
- {Connection.host(), pos_integer()}
- | {Connection.proxy_type(), Connection.host(), pos_integer()}
+ {host(), pos_integer()}
+ | {proxy_type(), host(), pos_integer()}
@callback options(keyword(), URI.t()) :: keyword()
diff --git a/lib/pleroma/http/request_builder.ex b/lib/pleroma/http/request_builder.ex
@@ -54,12 +54,12 @@ defmodule Pleroma.HTTP.RequestBuilder do
@doc """
Add optional parameters to the request
"""
- @spec add_param(Request.t(), atom(), atom(), any()) :: Request.t()
+ @spec add_param(Request.t(), atom(), atom() | String.t(), any()) :: Request.t()
def add_param(request, :query, :query, values), do: %{request | query: values}
def add_param(request, :body, :body, value), do: %{request | body: value}
- def add_param(request, :body, key, value) do
+ def add_param(request, :body, key, value) when is_binary(key) do
request
|> Map.put(:body, Multipart.new())
|> Map.update!(
diff --git a/lib/pleroma/instances.ex b/lib/pleroma/instances.ex
@@ -7,16 +7,15 @@ defmodule Pleroma.Instances do
alias Pleroma.Instances.Instance
- def filter_reachable(urls_or_hosts), do: Instance.filter_reachable(urls_or_hosts)
+ defdelegate filter_reachable(urls_or_hosts), to: Instance
- def reachable?(url_or_host), do: Instance.reachable?(url_or_host)
+ defdelegate reachable?(url_or_host), to: Instance
- def set_reachable(url_or_host), do: Instance.set_reachable(url_or_host)
+ defdelegate set_reachable(url_or_host), to: Instance
- def set_unreachable(url_or_host, unreachable_since \\ nil),
- do: Instance.set_unreachable(url_or_host, unreachable_since)
+ defdelegate set_unreachable(url_or_host, unreachable_since \\ nil), to: Instance
- def get_consistently_unreachable, do: Instance.get_consistently_unreachable()
+ defdelegate get_consistently_unreachable, to: Instance
def set_consistently_unreachable(url_or_host),
do: set_unreachable(url_or_host, reachability_datetime_threshold())
diff --git a/lib/pleroma/maps.ex b/lib/pleroma/maps.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Maps do
@@ -18,4 +18,17 @@ defmodule Pleroma.Maps do
rescue
_ -> data
end
+
+ def filter_empty_values(data) do
+ # TODO: Change to Map.filter in Elixir 1.13+
+ data
+ |> Enum.filter(fn
+ {_k, nil} -> false
+ {_k, ""} -> false
+ {_k, []} -> false
+ {_k, %{} = v} -> Map.keys(v) != []
+ {_k, _v} -> true
+ end)
+ |> Map.new()
+ end
end
diff --git a/lib/pleroma/mfa.ex b/lib/pleroma/mfa.ex
@@ -77,7 +77,7 @@ defmodule Pleroma.MFA do
{:ok, codes}
else
{:error, msg} ->
- %{error: msg}
+ {:error, msg}
end
end
diff --git a/lib/pleroma/mfa/totp.ex b/lib/pleroma/mfa/totp.ex
@@ -14,6 +14,7 @@ defmodule Pleroma.MFA.TOTP do
@doc """
https://github.com/google/google-authenticator/wiki/Key-Uri-Format
"""
+ @spec provisioning_uri(String.t(), String.t(), list()) :: String.t()
def provisioning_uri(secret, label, opts \\ []) do
query =
%{
@@ -27,7 +28,7 @@ defmodule Pleroma.MFA.TOTP do
|> URI.encode_query()
%URI{scheme: "otpauth", host: "totp", path: "/" <> label, query: query}
- |> URI.to_string()
+ |> to_string()
end
defp default_period, do: Config.get(@config_ns ++ [:period])
diff --git a/lib/pleroma/migrators/hashtags_table_migrator.ex b/lib/pleroma/migrators/hashtags_table_migrator.ex
@@ -100,7 +100,7 @@ defmodule Pleroma.Migrators.HashtagsTableMigrator do
|> where([_o, hashtags_objects], is_nil(hashtags_objects.object_id))
end
- @spec transfer_object_hashtags(Map.t()) :: {:noop | :ok | :error, integer()}
+ @spec transfer_object_hashtags(map()) :: {:noop | :ok | :error, integer()}
defp transfer_object_hashtags(object) do
embedded_tags = if Map.has_key?(object, :tag), do: object.tag, else: object.data["tag"]
hashtags = Object.object_data_hashtags(%{"tag" => embedded_tags})
diff --git a/lib/pleroma/migrators/support/base_migrator.ex b/lib/pleroma/migrators/support/base_migrator.ex
@@ -188,10 +188,11 @@ defmodule Pleroma.Migrators.Support.BaseMigrator do
end
defp fault_rate do
- with failures_count when is_integer(failures_count) <- failures_count() do
+ with failures_count when is_integer(failures_count) <- failures_count(),
+ true <- failures_count > 0 do
failures_count / Enum.max([get_stat(:affected_count, 0), 1])
else
- _ -> :error
+ _ -> 0
end
end
diff --git a/lib/pleroma/moderation_log.ex b/lib/pleroma/moderation_log.ex
@@ -121,7 +121,7 @@ defmodule Pleroma.ModerationLog do
defp prepare_log_data(attrs), do: attrs
- @spec insert_log(log_params()) :: {:ok, ModerationLog} | {:error, any}
+ @spec insert_log(log_params()) :: {:ok, ModerationLog.t()} | {:error, any}
def insert_log(%{actor: %User{}, subject: subjects, permission: permission} = attrs) do
data =
attrs
@@ -248,7 +248,8 @@ defmodule Pleroma.ModerationLog do
|> insert_log_entry_with_message()
end
- @spec insert_log_entry_with_message(ModerationLog) :: {:ok, ModerationLog} | {:error, any}
+ @spec insert_log_entry_with_message(ModerationLog.t()) ::
+ {:ok, ModerationLog.t()} | {:error, any}
defp insert_log_entry_with_message(entry) do
entry.data["message"]
|> put_in(get_log_entry_message(entry))
diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex
@@ -88,7 +88,7 @@ defmodule Pleroma.Notification do
where: q.seen == true,
select: type(q.id, :string),
limit: 1,
- order_by: [desc: :id]
+ order_by: fragment("? desc nulls last", q.id)
)
end
@@ -137,7 +137,7 @@ defmodule Pleroma.Notification do
blocked_ap_ids = opts[:blocked_users_ap_ids] || User.blocked_users_ap_ids(user)
query
- |> where([n, a], a.actor not in ^blocked_ap_ids)
+ |> where([..., user_actor: user_actor], user_actor.ap_id not in ^blocked_ap_ids)
|> FollowingRelationship.keep_following_or_not_domain_blocked(user)
end
@@ -148,7 +148,7 @@ defmodule Pleroma.Notification do
blocker_ap_ids = User.incoming_relationships_ungrouped_ap_ids(user, [:block])
query
- |> where([n, a], a.actor not in ^blocker_ap_ids)
+ |> where([..., user_actor: user_actor], user_actor.ap_id not in ^blocker_ap_ids)
end
end
@@ -161,7 +161,7 @@ defmodule Pleroma.Notification do
opts[:notification_muted_users_ap_ids] || User.notification_muted_users_ap_ids(user)
query
- |> where([n, a], a.actor not in ^notification_muted_ap_ids)
+ |> where([..., user_actor: user_actor], user_actor.ap_id not in ^notification_muted_ap_ids)
|> join(:left, [n, a], tm in ThreadMute,
on: tm.user_id == ^user.id and tm.context == fragment("?->>'context'", a.data),
as: :thread_mute
@@ -280,15 +280,10 @@ defmodule Pleroma.Notification do
select: n.id
)
- {:ok, %{ids: {_, notification_ids}}} =
- Multi.new()
- |> Multi.update_all(:ids, query, set: [seen: true, updated_at: NaiveDateTime.utc_now()])
- |> Marker.multi_set_last_read_id(user, "notifications")
- |> Repo.transaction()
-
- for_user_query(user)
- |> where([n], n.id in ^notification_ids)
- |> Repo.all()
+ Multi.new()
+ |> Multi.update_all(:ids, query, set: [seen: true, updated_at: NaiveDateTime.utc_now()])
+ |> Marker.multi_set_last_read_id(user, "notifications")
+ |> Repo.transaction()
end
@spec read_one(User.t(), String.t()) ::
@@ -299,10 +294,6 @@ defmodule Pleroma.Notification do
|> Multi.update(:update, changeset(notification, %{seen: true}))
|> Marker.multi_set_last_read_id(user, "notifications")
|> Repo.transaction()
- |> case do
- {:ok, %{update: notification}} -> {:ok, notification}
- {:error, :update, changeset, _} -> {:error, changeset}
- end
end
end
@@ -361,36 +352,32 @@ defmodule Pleroma.Notification do
end
end
- @spec create_notifications(Activity.t(), keyword()) :: {:ok, [Notification.t()] | []}
- def create_notifications(activity, options \\ [])
+ @spec create_notifications(Activity.t()) :: {:ok, [Notification.t()] | []}
+ def create_notifications(activity)
- def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity, options) do
+ def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity) do
object = Object.normalize(activity, fetch: false)
if object && object.data["type"] == "Answer" do
{:ok, []}
else
- do_create_notifications(activity, options)
+ do_create_notifications(activity)
end
end
- def create_notifications(%Activity{data: %{"type" => type}} = activity, options)
+ def create_notifications(%Activity{data: %{"type" => type}} = activity)
when type in ["Follow", "Like", "Announce", "Move", "EmojiReact", "Flag", "Update"] do
- do_create_notifications(activity, options)
+ do_create_notifications(activity)
end
- def create_notifications(_, _), do: {:ok, []}
-
- defp do_create_notifications(%Activity{} = activity, options) do
- do_send = Keyword.get(options, :do_send, true)
+ def create_notifications(_), do: {:ok, []}
- {enabled_receivers, disabled_receivers} = get_notified_from_activity(activity)
- potential_receivers = enabled_receivers ++ disabled_receivers
+ defp do_create_notifications(%Activity{} = activity) do
+ enabled_receivers = get_notified_from_activity(activity)
notifications =
- Enum.map(potential_receivers, fn user ->
- do_send = do_send && user in enabled_receivers
- create_notification(activity, user, do_send: do_send)
+ Enum.map(enabled_receivers, fn user ->
+ create_notification(activity, user)
end)
|> Enum.reject(&is_nil/1)
@@ -450,7 +437,6 @@ defmodule Pleroma.Notification do
# TODO move to sql, too.
def create_notification(%Activity{} = activity, %User{} = user, opts \\ []) do
- do_send = Keyword.get(opts, :do_send, true)
type = Keyword.get(opts, :type, type_from_activity(activity))
unless skip?(activity, user, opts) do
@@ -465,11 +451,6 @@ defmodule Pleroma.Notification do
|> Marker.multi_set_last_read_id(user, "notifications")
|> Repo.transaction()
- if do_send do
- Streamer.stream(["user", "user:notification"], notification)
- Push.send(notification)
- end
-
notification
end
end
@@ -527,10 +508,7 @@ defmodule Pleroma.Notification do
|> exclude_relationship_restricted_ap_ids(activity)
|> exclude_thread_muter_ap_ids(activity)
- notification_enabled_users =
- Enum.filter(potential_receivers, fn u -> u.ap_id in notification_enabled_ap_ids end)
-
- {notification_enabled_users, potential_receivers -- notification_enabled_users}
+ Enum.filter(potential_receivers, fn u -> u.ap_id in notification_enabled_ap_ids end)
end
def get_notified_from_activity(_, _local_only), do: {[], []}
@@ -643,6 +621,7 @@ defmodule Pleroma.Notification do
def skip?(%Activity{} = activity, %User{} = user, opts) do
[
:self,
+ :internal,
:invisible,
:block_from_strangers,
:recently_followed,
@@ -662,6 +641,12 @@ defmodule Pleroma.Notification do
end
end
+ def skip?(:internal, %Activity{} = activity, _user, _opts) do
+ actor = activity.data["actor"]
+ user = User.get_cached_by_ap_id(actor)
+ User.internal?(user)
+ end
+
def skip?(:invisible, %Activity{} = activity, _user, _opts) do
actor = activity.data["actor"]
user = User.get_cached_by_ap_id(actor)
@@ -748,4 +733,12 @@ defmodule Pleroma.Notification do
)
|> Repo.update_all(set: [seen: true])
end
+
+ @spec send(list(Notification.t())) :: :ok
+ def send(notifications) do
+ Enum.each(notifications, fn notification ->
+ Streamer.stream(["user", "user:notification"], notification)
+ Push.send(notification)
+ end)
+ end
end
diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex
@@ -177,7 +177,10 @@ defmodule Pleroma.Object do
ap_id
Keyword.get(options, :fetch) ->
- Fetcher.fetch_object_from_id!(ap_id, options)
+ case Fetcher.fetch_object_from_id(ap_id, options) do
+ {:ok, object} -> object
+ _ -> nil
+ end
true ->
get_cached_by_ap_id(ap_id)
@@ -239,17 +242,17 @@ defmodule Pleroma.Object do
{:ok, _} <- invalid_object_cache(object) do
cleanup_attachments(
Config.get([:instance, :cleanup_attachments]),
- %{"object" => object}
+ object
)
{:ok, object, deleted_activity}
end
end
- @spec cleanup_attachments(boolean(), %{required(:object) => map()}) ::
+ @spec cleanup_attachments(boolean(), Object.t()) ::
{:ok, Oban.Job.t() | nil}
- def cleanup_attachments(true, %{"object" => _} = params) do
- AttachmentsCleanupWorker.enqueue("cleanup_attachments", params)
+ def cleanup_attachments(true, %Object{} = object) do
+ AttachmentsCleanupWorker.enqueue("cleanup_attachments", %{"object" => object})
end
def cleanup_attachments(_, _), do: {:ok, nil}
diff --git a/lib/pleroma/object/fetcher.ex b/lib/pleroma/object/fetcher.ex
@@ -72,20 +72,25 @@ defmodule Pleroma.Object.Fetcher do
{:object, data, Object.normalize(activity, fetch: false)} do
{:ok, object}
else
- {:allowed_depth, false} ->
- {:error, "Max thread distance exceeded."}
+ {:allowed_depth, false} = e ->
+ log_fetch_error(id, e)
+ {:error, :allowed_depth}
- {:containment, _} ->
- {:error, "Object containment failed."}
+ {:containment, reason} = e ->
+ log_fetch_error(id, e)
+ {:error, reason}
- {:transmogrifier, {:error, {:reject, e}}} ->
- {:reject, e}
+ {:transmogrifier, {:error, {:reject, reason}}} = e ->
+ log_fetch_error(id, e)
+ {:reject, reason}
- {:transmogrifier, {:reject, e}} ->
- {:reject, e}
+ {:transmogrifier, {:reject, reason}} = e ->
+ log_fetch_error(id, e)
+ {:reject, reason}
- {:transmogrifier, _} = e ->
- {:error, e}
+ {:transmogrifier, reason} = e ->
+ log_fetch_error(id, e)
+ {:error, reason}
{:object, data, nil} ->
reinject_object(%Object{}, data)
@@ -96,14 +101,21 @@ defmodule Pleroma.Object.Fetcher do
{:fetch_object, %Object{} = object} ->
{:ok, object}
- {:fetch, {:error, error}} ->
- {:error, error}
+ {:fetch, {:error, reason}} = e ->
+ log_fetch_error(id, e)
+ {:error, reason}
e ->
- e
+ log_fetch_error(id, e)
+ {:error, e}
end
end
+ defp log_fetch_error(id, error) do
+ Logger.metadata(object: id)
+ Logger.error("Object rejected while fetching #{id} #{inspect(error)}")
+ end
+
defp prepare_activity_params(data) do
%{
"type" => "Create",
@@ -117,26 +129,6 @@ defmodule Pleroma.Object.Fetcher do
|> Maps.put_if_present("bcc", data["bcc"])
end
- def fetch_object_from_id!(id, options \\ []) do
- with {:ok, object} <- fetch_object_from_id(id, options) do
- object
- else
- {:error, %Tesla.Mock.Error{}} ->
- nil
-
- {:error, "Object has been deleted"} ->
- nil
-
- {:reject, reason} ->
- Logger.info("Rejected #{id} while fetching: #{inspect(reason)}")
- nil
-
- e ->
- Logger.error("Error while fetching #{id}: #{inspect(e)}")
- nil
- end
- end
-
defp make_signature(id, date) do
uri = URI.parse(id)
@@ -227,8 +219,11 @@ defmodule Pleroma.Object.Fetcher do
{:error, {:content_type, nil}}
end
+ {:ok, %{status: code}} when code in [401, 403] ->
+ {:error, :forbidden}
+
{:ok, %{status: code}} when code in [404, 410] ->
- {:error, "Object has been deleted"}
+ {:error, :not_found}
{:error, e} ->
{:error, e}
diff --git a/lib/pleroma/pagination.ex b/lib/pleroma/pagination.ex
@@ -61,15 +61,16 @@ defmodule Pleroma.Pagination do
|> Repo.all()
end
- @spec paginate(Ecto.Query.t(), map(), type(), atom() | nil) :: [Ecto.Schema.t()]
- def paginate(query, options, method \\ :keyset, table_binding \\ nil)
-
- def paginate(list, options, _method, _table_binding) when is_list(list) do
+ @spec paginate_list(list(), keyword()) :: list()
+ def paginate_list(list, options) do
offset = options[:offset] || 0
limit = options[:limit] || 0
Enum.slice(list, offset, limit)
end
+ @spec paginate(Ecto.Query.t(), map(), type(), atom() | nil) :: [Ecto.Schema.t()]
+ def paginate(query, options, method \\ :keyset, table_binding \\ nil)
+
def paginate(query, options, :keyset, table_binding) do
query
|> restrict(:min_id, options, table_binding)
diff --git a/lib/pleroma/password/pbkdf2.ex b/lib/pleroma/password/pbkdf2.ex
@@ -28,7 +28,7 @@ defmodule Pleroma.Password.Pbkdf2 do
iterations = String.to_integer(iterations)
- digest = String.to_atom(digest)
+ digest = String.to_existing_atom(digest)
binary_hash =
KeyGenerator.generate(password, salt, digest: digest, iterations: iterations, length: 64)
diff --git a/lib/pleroma/release_tasks.ex b/lib/pleroma/release_tasks.ex
@@ -55,12 +55,6 @@ defmodule Pleroma.ReleaseTasks do
{:error, term} when is_binary(term) ->
IO.puts(:stderr, "The database for #{inspect(@repo)} couldn't be created: #{term}")
-
- {:error, term} ->
- IO.puts(
- :stderr,
- "The database for #{inspect(@repo)} couldn't be created: #{inspect(term)}"
- )
end
end
end
diff --git a/lib/pleroma/report_note.ex b/lib/pleroma/report_note.ex
@@ -23,8 +23,8 @@ defmodule Pleroma.ReportNote do
timestamps()
end
- @spec create(FlakeId.Ecto.CompatType.t(), FlakeId.Ecto.CompatType.t(), String.t()) ::
- {:ok, ReportNote.t()} | {:error, Changeset.t()}
+ @spec create(Ecto.UUID.t(), Ecto.UUID.t(), String.t()) ::
+ {:ok, ReportNote.t()} | {:error, Ecto.Changeset.t()}
def create(user_id, activity_id, content) do
attrs = %{
user_id: user_id,
@@ -38,8 +38,8 @@ defmodule Pleroma.ReportNote do
|> Repo.insert()
end
- @spec destroy(FlakeId.Ecto.CompatType.t()) ::
- {:ok, ReportNote.t()} | {:error, Changeset.t()}
+ @spec destroy(Ecto.UUID.t()) ::
+ {:ok, ReportNote.t()} | {:error, Ecto.Changeset.t()}
def destroy(id) do
from(r in ReportNote, where: r.id == ^id)
|> Repo.one()
diff --git a/lib/pleroma/reverse_proxy.ex b/lib/pleroma/reverse_proxy.ex
@@ -8,7 +8,7 @@ defmodule Pleroma.ReverseProxy do
~w(if-unmodified-since if-none-match) ++ @range_headers
@resp_cache_headers ~w(etag date last-modified)
@keep_resp_headers @resp_cache_headers ++
- ~w(content-length content-type content-disposition content-encoding) ++
+ ~w(content-type content-disposition content-encoding) ++
~w(content-range accept-ranges vary)
@default_cache_control_header "public, max-age=1209600"
@valid_resp_codes [200, 206, 304]
@@ -81,16 +81,16 @@ defmodule Pleroma.ReverseProxy do
import Plug.Conn
@type option() ::
- {:max_read_duration, :timer.time() | :infinity}
+ {:max_read_duration, non_neg_integer() | :infinity}
| {:max_body_length, non_neg_integer() | :infinity}
- | {:failed_request_ttl, :timer.time() | :infinity}
- | {:http, []}
+ | {:failed_request_ttl, non_neg_integer() | :infinity}
+ | {:http, keyword()}
| {:req_headers, [{String.t(), String.t()}]}
| {:resp_headers, [{String.t(), String.t()}]}
- | {:inline_content_types, boolean() | [String.t()]}
+ | {:inline_content_types, boolean() | list(String.t())}
| {:redirect_on_failure, boolean()}
- @spec call(Plug.Conn.t(), url :: String.t(), [option()]) :: Plug.Conn.t()
+ @spec call(Plug.Conn.t(), String.t(), list(option())) :: Plug.Conn.t()
def call(_conn, _url, _opts \\ [])
def call(conn = %{method: method}, url, opts) when method in @methods do
@@ -388,8 +388,6 @@ defmodule Pleroma.ReverseProxy do
defp body_size_constraint(_, _), do: :ok
- defp check_read_duration(nil = _duration, max), do: check_read_duration(@max_read_duration, max)
-
defp check_read_duration(duration, max)
when is_integer(duration) and is_integer(max) and max > 0 do
if duration > max do
@@ -407,10 +405,6 @@ defmodule Pleroma.ReverseProxy do
{:ok, previous_duration + duration}
end
- defp increase_read_duration(_) do
- {:ok, :no_duration_limit, :no_duration_limit}
- end
-
defp client, do: Pleroma.ReverseProxy.Client.Wrapper
defp track_failed_url(url, error, opts) do
diff --git a/lib/pleroma/rule.ex b/lib/pleroma/rule.ex
@@ -0,0 +1,68 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Rule do
+ use Ecto.Schema
+
+ import Ecto.Changeset
+ import Ecto.Query
+
+ alias Pleroma.Repo
+ alias Pleroma.Rule
+
+ schema "rules" do
+ field(:priority, :integer, default: 0)
+ field(:text, :string)
+ field(:hint, :string)
+
+ timestamps()
+ end
+
+ def changeset(%Rule{} = rule, params \\ %{}) do
+ rule
+ |> cast(params, [:priority, :text, :hint])
+ |> validate_required([:text])
+ end
+
+ def query do
+ Rule
+ |> order_by(asc: :priority)
+ |> order_by(asc: :id)
+ end
+
+ def get(ids) when is_list(ids) do
+ from(r in __MODULE__, where: r.id in ^ids)
+ |> Repo.all()
+ end
+
+ def get(id), do: Repo.get(__MODULE__, id)
+
+ def exists?(id) do
+ from(r in __MODULE__, where: r.id == ^id)
+ |> Repo.exists?()
+ end
+
+ def create(params) do
+ {:ok, rule} =
+ %Rule{}
+ |> changeset(params)
+ |> Repo.insert()
+
+ rule
+ end
+
+ def update(params, id) do
+ {:ok, rule} =
+ get(id)
+ |> changeset(params)
+ |> Repo.update()
+
+ rule
+ end
+
+ def delete(id) do
+ get(id)
+ |> Repo.delete()
+ end
+end
diff --git a/lib/pleroma/search/database_search.ex b/lib/pleroma/search/database_search.ex
@@ -23,19 +23,12 @@ defmodule Pleroma.Search.DatabaseSearch do
offset = Keyword.get(options, :offset, 0)
author = Keyword.get(options, :author)
- search_function =
- if :persistent_term.get({Pleroma.Repo, :postgres_version}) >= 11 do
- :websearch
- else
- :plain
- end
-
try do
Activity
|> Activity.with_preloaded_object()
|> Activity.restrict_deactivated_users()
|> restrict_public(user)
- |> query_with(index_type, search_query, search_function)
+ |> query_with(index_type, search_query, :websearch)
|> maybe_restrict_local(user)
|> maybe_restrict_author(author)
|> maybe_restrict_blocked(user)
diff --git a/lib/pleroma/search/search_backend.ex b/lib/pleroma/search/search_backend.ex
@@ -17,8 +17,8 @@ defmodule Pleroma.Search.SearchBackend do
Remove the object from the index.
Just the object, as opposed to the whole activity, is passed, since the object
- is what contains the actual content and there is no need for fitlering when removing
+ is what contains the actual content and there is no need for filtering when removing
from index.
"""
- @callback remove_from_index(object :: Pleroma.Object.t()) :: {:ok, any()} | {:error, any()}
+ @callback remove_from_index(object :: Pleroma.Object.t()) :: :ok | {:error, any()}
end
diff --git a/lib/pleroma/signature.ex b/lib/pleroma/signature.ex
@@ -27,7 +27,7 @@ defmodule Pleroma.Signature do
_ ->
case Pleroma.Web.WebFinger.finger(maybe_ap_id) do
- %{"ap_id" => ap_id} -> {:ok, ap_id}
+ {:ok, %{"ap_id" => ap_id}} -> {:ok, ap_id}
_ -> {:error, maybe_ap_id}
end
end
diff --git a/lib/pleroma/telemetry/logger.ex b/lib/pleroma/telemetry/logger.ex
@@ -59,7 +59,7 @@ defmodule Pleroma.Telemetry.Logger do
_,
_
) do
- Logger.error(fn ->
+ Logger.debug(fn ->
"Connection pool had to refuse opening a connection to #{key} due to connection limit exhaustion"
end)
end
@@ -81,7 +81,7 @@ defmodule Pleroma.Telemetry.Logger do
%{key: key, protocol: :http},
_
) do
- Logger.info(fn ->
+ Logger.debug(fn ->
"Pool worker for #{key}: #{length(clients)} clients are using an HTTP1 connection at the same time, head-of-line blocking might occur."
end)
end
diff --git a/lib/pleroma/upload.ex b/lib/pleroma/upload.ex
@@ -51,6 +51,7 @@ defmodule Pleroma.Upload do
| {:size_limit, nil | non_neg_integer()}
| {:uploader, module()}
| {:filters, [module()]}
+ | {:actor, String.t()}
@type t :: %__MODULE__{
id: String.t(),
@@ -86,7 +87,7 @@ defmodule Pleroma.Upload do
end
end
- @spec store(source, options :: [option()]) :: {:ok, Map.t()} | {:error, any()}
+ @spec store(source, options :: [option()]) :: {:ok, map()} | {:error, any()}
@doc "Store a file. If using a `Plug.Upload{}` as the source, be sure to use `Majic.Plug` to ensure its content_type and filename is correct."
def store(upload, opts \\ []) do
opts = get_opts(opts)
@@ -175,7 +176,7 @@ defmodule Pleroma.Upload do
defp prepare_upload(%{img: "data:image/" <> image_data}, opts) do
parsed = Regex.named_captures(~r/(?<filetype>jpeg|png|gif);base64,(?<data>.*)/, image_data)
data = Base.decode64!(parsed["data"], ignore: :whitespace)
- hash = Base.encode16(:crypto.hash(:sha256, data), lower: true)
+ hash = Base.encode16(:crypto.hash(:sha256, data), case: :upper)
with :ok <- check_binary_size(data, opts.size_limit),
tmp_path <- tempfile_for_image(data),
diff --git a/lib/pleroma/uploaders/uploader.ex b/lib/pleroma/uploaders/uploader.ex
@@ -5,8 +5,6 @@
defmodule Pleroma.Uploaders.Uploader do
import Pleroma.Web.Gettext
- @mix_env Mix.env()
-
@moduledoc """
Defines the contract to put and get an uploaded file to any backend.
"""
@@ -40,7 +38,7 @@ defmodule Pleroma.Uploaders.Uploader do
@callback delete_file(file :: String.t()) :: :ok | {:error, String.t()}
- @callback http_callback(Plug.Conn.t(), Map.t()) ::
+ @callback http_callback(Plug.Conn.t(), map()) ::
{:ok, Plug.Conn.t()}
| {:ok, Plug.Conn.t(), file_spec()}
| {:error, Plug.Conn.t(), String.t()}
@@ -75,10 +73,5 @@ defmodule Pleroma.Uploaders.Uploader do
end
end
- defp callback_timeout do
- case @mix_env do
- :test -> 1_000
- _ -> 30_000
- end
- end
+ defp callback_timeout, do: Application.get_env(:pleroma, __MODULE__)[:timeout]
end
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
@@ -8,6 +8,7 @@ defmodule Pleroma.User do
import Ecto.Changeset
import Ecto.Query
import Ecto, only: [assoc: 2]
+ import Pleroma.Web.Utils.Guards, only: [not_empty_string: 1]
alias Ecto.Multi
alias Pleroma.Activity
@@ -39,6 +40,7 @@ defmodule Pleroma.User do
alias Pleroma.Workers.BackgroundWorker
require Logger
+ require Pleroma.Constants
@type t :: %__MODULE__{}
@type account_status ::
@@ -579,7 +581,7 @@ defmodule Pleroma.User do
|> validate_format(:nickname, local_nickname_regex())
|> validate_length(:bio, max: bio_limit)
|> validate_length(:name, min: 1, max: name_limit)
- |> validate_inclusion(:actor_type, ["Person", "Service"])
+ |> validate_inclusion(:actor_type, Pleroma.Constants.allowed_user_actor_types())
|> put_fields()
|> put_emoji()
|> put_change_if_present(:bio, &{:ok, parse_bio(&1, struct)})
@@ -595,9 +597,23 @@ defmodule Pleroma.User do
defp put_fields(changeset) do
if raw_fields = get_change(changeset, :raw_fields) do
+ old_fields = changeset.data.raw_fields
+
raw_fields =
raw_fields
|> Enum.filter(fn %{"name" => n} -> n != "" end)
+ |> Enum.map(fn field ->
+ previous =
+ old_fields
+ |> Enum.find(fn %{"value" => value} -> field["value"] == value end)
+
+ if previous && Map.has_key?(previous, "verified_at") do
+ field
+ |> Map.put("verified_at", previous["verified_at"])
+ else
+ field
+ end
+ end)
fields =
raw_fields
@@ -671,7 +687,7 @@ defmodule Pleroma.User do
|> validate_inclusion(:actor_type, ["Person", "Service"])
end
- @spec update_as_admin(User.t(), map()) :: {:ok, User.t()} | {:error, Changeset.t()}
+ @spec update_as_admin(User.t(), map()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
def update_as_admin(user, params) do
params = Map.put(params, "password_confirmation", params["password"])
changeset = update_as_admin_changeset(user, params)
@@ -692,7 +708,7 @@ defmodule Pleroma.User do
|> put_change(:password_reset_pending, false)
end
- @spec reset_password(User.t(), map()) :: {:ok, User.t()} | {:error, Changeset.t()}
+ @spec reset_password(User.t(), map()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
def reset_password(%User{} = user, params) do
reset_password(user, user, params)
end
@@ -1010,7 +1026,7 @@ defmodule Pleroma.User do
def maybe_send_confirmation_email(_), do: {:ok, :noop}
- @spec send_confirmation_email(Uset.t()) :: User.t()
+ @spec send_confirmation_email(User.t()) :: User.t()
def send_confirmation_email(%User{} = user) do
user
|> Pleroma.Emails.UserEmail.account_confirmation_email()
@@ -1047,7 +1063,8 @@ defmodule Pleroma.User do
def needs_update?(_), do: true
- @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
+ @spec maybe_direct_follow(User.t(), User.t()) ::
+ {:ok, User.t(), User.t()} | {:error, String.t()}
# "Locked" (self-locked) users demand explicit authorization of follow requests
def maybe_direct_follow(%User{} = follower, %User{local: true, is_locked: true} = followed) do
@@ -1198,6 +1215,10 @@ defmodule Pleroma.User do
def update_and_set_cache(changeset) do
with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
+ if get_change(changeset, :raw_fields) do
+ BackgroundWorker.enqueue("verify_fields_links", %{"user_id" => user.id})
+ end
+
set_cache(user)
end
end
@@ -1383,6 +1404,40 @@ defmodule Pleroma.User do
|> Repo.all()
end
+ @spec get_familiar_followers_query(User.t(), User.t(), pos_integer() | nil) :: Ecto.Query.t()
+ def get_familiar_followers_query(%User{} = user, %User{} = current_user, nil) do
+ friends =
+ get_friends_query(current_user)
+ |> where([u], not u.hide_follows)
+ |> select([u], u.id)
+
+ User.Query.build(%{is_active: true})
+ |> where([u], u.id not in ^[user.id, current_user.id])
+ |> join(:inner, [u], r in FollowingRelationship,
+ as: :followers_relationships,
+ on: r.following_id == ^user.id and r.follower_id == u.id
+ )
+ |> where([followers_relationships: r], r.state == ^:follow_accept)
+ |> where([followers_relationships: r], r.follower_id in subquery(friends))
+ end
+
+ def get_familiar_followers_query(%User{} = user, %User{} = current_user, page) do
+ user
+ |> get_familiar_followers_query(current_user, nil)
+ |> User.Query.paginate(page, 20)
+ end
+
+ @spec get_familiar_followers_query(User.t(), User.t()) :: Ecto.Query.t()
+ def get_familiar_followers_query(%User{} = user, %User{} = current_user),
+ do: get_familiar_followers_query(user, current_user, nil)
+
+ @spec get_familiar_followers(User.t(), User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
+ def get_familiar_followers(%User{} = user, %User{} = current_user, page \\ nil) do
+ user
+ |> get_familiar_followers_query(current_user, page)
+ |> Repo.all()
+ end
+
def increase_note_count(%User{} = user) do
User
|> where(id: ^user.id)
@@ -1782,14 +1837,17 @@ defmodule Pleroma.User do
BackgroundWorker.enqueue("user_activation", %{"user_id" => user.id, "status" => status})
end
- @spec set_activation([User.t()], boolean()) :: {:ok, User.t()} | {:error, Changeset.t()}
+ @spec set_activation([User.t()], boolean()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
def set_activation(users, status) when is_list(users) do
Repo.transaction(fn ->
- for user <- users, do: set_activation(user, status)
+ for user <- users do
+ {:ok, user} = set_activation(user, status)
+ user
+ end
end)
end
- @spec set_activation(User.t(), boolean()) :: {:ok, User.t()} | {:error, Changeset.t()}
+ @spec set_activation(User.t(), boolean()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
def set_activation(%User{} = user, status) do
with {:ok, user} <- set_activation_status(user, status) do
user
@@ -1867,7 +1925,7 @@ defmodule Pleroma.User do
|> update_and_set_cache()
end
- @spec purge_user_changeset(User.t()) :: Changeset.t()
+ @spec purge_user_changeset(User.t()) :: Ecto.Changeset.t()
def purge_user_changeset(user) do
# "Right to be forgotten"
# https://gdpr.eu/right-to-be-forgotten/
@@ -1970,8 +2028,45 @@ defmodule Pleroma.User do
maybe_delete_from_db(user)
end
+ def perform(:verify_fields_links, user) do
+ profile_urls = [user.ap_id]
+
+ fields =
+ user.raw_fields
+ |> Enum.map(&verify_field_link(&1, profile_urls))
+
+ changeset =
+ user
+ |> update_changeset(%{raw_fields: fields})
+
+ with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
+ set_cache(user)
+ end
+ end
+
def perform(:set_activation_async, user, status), do: set_activation(user, status)
+ defp verify_field_link(field, profile_urls) do
+ verified_at =
+ with %{"value" => value} <- field,
+ {:verified_at, nil} <- {:verified_at, Map.get(field, "verified_at")},
+ %{scheme: scheme, userinfo: nil, host: host}
+ when not_empty_string(host) and scheme in ["http", "https"] <-
+ URI.parse(value),
+ {:not_idn, true} <- {:not_idn, to_string(:idna.encode(host)) == host},
+ "me" <- Pleroma.Web.RelMe.maybe_put_rel_me(value, profile_urls) do
+ CommonUtils.to_masto_date(NaiveDateTime.utc_now())
+ else
+ {:verified_at, value} when not_empty_string(value) ->
+ value
+
+ _ ->
+ nil
+ end
+
+ Map.put(field, "verified_at", verified_at)
+ end
+
@spec external_users_query() :: Ecto.Query.t()
def external_users_query do
User.Query.build(%{
@@ -2252,7 +2347,7 @@ defmodule Pleroma.User do
if String.contains?(user.nickname, "@") do
user.nickname
else
- %{host: host} = URI.parse(user.ap_id)
+ host = Pleroma.Web.WebFinger.host()
user.nickname <> "@" <> host
end
end
@@ -2358,7 +2453,7 @@ defmodule Pleroma.User do
updated_user
end
- @spec set_confirmation(User.t(), boolean()) :: {:ok, User.t()} | {:error, Changeset.t()}
+ @spec set_confirmation(User.t(), boolean()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
def set_confirmation(%User{} = user, bool) do
user
|> confirmation_changeset(set_confirmation: bool)
@@ -2402,9 +2497,9 @@ defmodule Pleroma.User do
defp put_password_hash(changeset), do: changeset
- def is_internal_user?(%User{nickname: nil}), do: true
- def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
- def is_internal_user?(_), do: false
+ def internal?(%User{nickname: nil}), do: true
+ def internal?(%User{local: true, nickname: "internal." <> _}), do: true
+ def internal?(_), do: false
# A hack because user delete activities have a fake id for whatever reason
# TODO: Get rid of this
@@ -2536,7 +2631,7 @@ defmodule Pleroma.User do
|> update_and_set_cache()
end
- @spec confirmation_changeset(User.t(), keyword()) :: Changeset.t()
+ @spec confirmation_changeset(User.t(), keyword()) :: Ecto.Changeset.t()
def confirmation_changeset(user, set_confirmation: confirmed?) do
params =
if confirmed? do
@@ -2554,9 +2649,9 @@ defmodule Pleroma.User do
cast(user, params, [:is_confirmed, :confirmation_token])
end
- @spec approval_changeset(User.t(), keyword()) :: Changeset.t()
- def approval_changeset(user, set_approval: approved?) do
- cast(user, %{is_approved: approved?}, [:is_approved])
+ @spec approval_changeset(Ecto.Changeset.t(), keyword()) :: Ecto.Changeset.t()
+ def approval_changeset(changeset, set_approval: approved?) do
+ cast(changeset, %{is_approved: approved?}, [:is_approved])
end
@spec add_pinned_object_id(User.t(), String.t()) :: {:ok, User.t()} | {:error, term()}
@@ -2659,10 +2754,11 @@ defmodule Pleroma.User do
# - display name
def sanitize_html(%User{} = user, filter) do
fields =
- Enum.map(user.fields, fn %{"name" => name, "value" => value} ->
+ Enum.map(user.fields, fn %{"name" => name, "value" => value} = fields ->
%{
"name" => name,
- "value" => HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
+ "value" => HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly),
+ "verified_at" => Map.get(fields, "verified_at")
}
end)
diff --git a/lib/pleroma/user/backup.ex b/lib/pleroma/user/backup.ex
@@ -22,6 +22,8 @@ defmodule Pleroma.User.Backup do
alias Pleroma.Web.ActivityPub.UserView
alias Pleroma.Workers.BackupWorker
+ @type t :: %__MODULE__{}
+
schema "backups" do
field(:content_type, :string)
field(:file_name, :string)
@@ -194,7 +196,15 @@ defmodule Pleroma.User.Backup do
end
end
- @files ['actor.json', 'outbox.json', 'likes.json', 'bookmarks.json']
+ @files [
+ 'actor.json',
+ 'outbox.json',
+ 'likes.json',
+ 'bookmarks.json',
+ 'followers.json',
+ 'following.json'
+ ]
+ @spec export(Pleroma.User.Backup.t(), pid()) :: {:ok, String.t()} | :error
def export(%__MODULE__{} = backup, caller_pid) do
backup = Repo.preload(backup, :user)
dir = backup_tempdir(backup)
@@ -204,9 +214,13 @@ defmodule Pleroma.User.Backup do
:ok <- statuses(dir, backup.user, caller_pid),
:ok <- likes(dir, backup.user, caller_pid),
:ok <- bookmarks(dir, backup.user, caller_pid),
- {:ok, zip_path} <- :zip.create(String.to_charlist(dir <> ".zip"), @files, cwd: dir),
+ :ok <- followers(dir, backup.user, caller_pid),
+ :ok <- following(dir, backup.user, caller_pid),
+ {:ok, zip_path} <- :zip.create(backup.file_name, @files, cwd: dir),
{:ok, _} <- File.rm_rf(dir) do
- {:ok, to_string(zip_path)}
+ {:ok, zip_path}
+ else
+ _ -> :error
end
end
@@ -352,6 +366,16 @@ defmodule Pleroma.User.Backup do
caller_pid
)
end
+
+ defp followers(dir, user, caller_pid) do
+ User.get_followers_query(user)
+ |> write(dir, "followers", fn a -> {:ok, a.ap_id} end, caller_pid)
+ end
+
+ defp following(dir, user, caller_pid) do
+ User.get_friends_query(user)
+ |> write(dir, "following", fn a -> {:ok, a.ap_id} end, caller_pid)
+ end
end
defmodule Pleroma.User.Backup.ProcessorAPI do
@@ -382,6 +406,8 @@ defmodule Pleroma.User.Backup.Processor do
[:file_size, :processed, :state]
)
|> Repo.update()
+ else
+ e -> {:error, e}
end
end
end
diff --git a/lib/pleroma/user/query.ex b/lib/pleroma/user/query.ex
@@ -22,7 +22,7 @@ defmodule Pleroma.User.Query do
- pass non empty string
- e.g. Pleroma.User.Query.build(%{email: "email@example.com"})
- *contains criteria*
- - add field to @containns_criteria list
+ - add field to @contains_criteria list
- pass values list
- e.g. Pleroma.User.Query.build(%{ap_id: ["http://ap_id1", "http://ap_id2"]})
"""
@@ -71,7 +71,7 @@ defmodule Pleroma.User.Query do
@equal_criteria [:email]
@contains_criteria [:ap_id, :nickname]
- @spec build(Query.t(), criteria()) :: Query.t()
+ @spec build(Ecto.Query.t(), criteria()) :: Ecto.Query.t()
def build(query \\ base_query(), criteria) do
prepare_query(query, criteria)
end
diff --git a/lib/pleroma/user_invite_token.ex b/lib/pleroma/user_invite_token.ex
@@ -64,7 +64,7 @@ defmodule Pleroma.UserInviteToken do
end
@spec update_invite(UserInviteToken.t(), map()) ::
- {:ok, UserInviteToken.t()} | {:error, Changeset.t()}
+ {:ok, UserInviteToken.t()} | {:error, Ecto.Changeset.t()}
def update_invite(invite, changes) do
change(invite, changes) |> Repo.update()
end
diff --git a/lib/pleroma/user_relationship.ex b/lib/pleroma/user_relationship.ex
@@ -14,6 +14,8 @@ defmodule Pleroma.UserRelationship do
alias Pleroma.User
alias Pleroma.UserRelationship
+ @type t :: %__MODULE__{}
+
schema "user_relationships" do
belongs_to(:source, User, type: FlakeId.Ecto.CompatType)
belongs_to(:target, User, type: FlakeId.Ecto.CompatType)
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -74,22 +74,22 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp check_remote_limit(_), do: true
def increase_note_count_if_public(actor, object) do
- if is_public?(object), do: User.increase_note_count(actor), else: {:ok, actor}
+ if public?(object), do: User.increase_note_count(actor), else: {:ok, actor}
end
def decrease_note_count_if_public(actor, object) do
- if is_public?(object), do: User.decrease_note_count(actor), else: {:ok, actor}
+ if public?(object), do: User.decrease_note_count(actor), else: {:ok, actor}
end
def update_last_status_at_if_public(actor, object) do
- if is_public?(object), do: User.update_last_status_at(actor), else: {:ok, actor}
+ if public?(object), do: User.update_last_status_at(actor), else: {:ok, actor}
end
defp increase_replies_count_if_reply(%{
"object" => %{"inReplyTo" => reply_ap_id} = object,
"type" => "Create"
}) do
- if is_public?(object) do
+ if public?(object) do
Object.increase_replies_count(reply_ap_id)
end
end
@@ -100,7 +100,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
"object" => %{"quoteUrl" => quote_ap_id} = object,
"type" => "Create"
}) do
- if is_public?(object) do
+ if public?(object) do
Object.increase_quotes_count(quote_ap_id)
end
end
@@ -147,9 +147,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
# Splice in the child object if we have one.
activity = Maps.put_if_present(activity, :object, object)
- ConcurrentLimiter.limit(Pleroma.Web.RichMedia.Helpers, fn ->
- Task.start(fn -> Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity) end)
- end)
+ Pleroma.Web.RichMedia.Card.get_by_activity(activity)
# Add local posts to search index
if local, do: Pleroma.Search.add_to_index(activity)
@@ -177,7 +175,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
id: "pleroma:fakeid"
}
- Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
+ Pleroma.Web.RichMedia.Card.get_by_activity(activity)
{:ok, activity}
{:remote_limit_pass, _} ->
@@ -202,7 +200,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
def notify_and_stream(activity) do
- Notification.create_notifications(activity)
+ {:ok, notifications} = Notification.create_notifications(activity)
+ Notification.send(notifications)
original_activity =
case activity do
@@ -319,6 +318,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
{:ok, _actor} <- update_last_status_at_if_public(actor, activity),
_ <- notify_and_stream(activity),
:ok <- maybe_schedule_poll_notifications(activity),
+ :ok <- maybe_handle_group_posts(activity),
:ok <- maybe_federate(activity) do
{:ok, activity}
else
@@ -498,7 +498,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
@spec fetch_latest_direct_activity_id_for_context(String.t(), keyword() | map()) ::
- FlakeId.Ecto.CompatType.t() | nil
+ Ecto.UUID.t() | nil
def fetch_latest_direct_activity_id_for_context(context, opts \\ %{}) do
context
|> fetch_activities_for_context_query(Map.merge(%{skip_preload: true}, opts))
@@ -1260,6 +1260,15 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp restrict_quote_url(query, _), do: query
+ defp restrict_rule(query, %{rule_id: rule_id}) do
+ from(
+ activity in query,
+ where: fragment("(?)->'rules' \\? (?)", activity.data, ^rule_id)
+ )
+ end
+
+ defp restrict_rule(query, _), do: query
+
defp exclude_poll_votes(query, %{include_poll_votes: true}), do: query
defp exclude_poll_votes(query, _) do
@@ -1422,6 +1431,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> restrict_instance(opts)
|> restrict_announce_object_actor(opts)
|> restrict_filtered(opts)
+ |> restrict_rule(opts)
|> restrict_quote_url(opts)
|> maybe_restrict_deactivated_users(opts)
|> exclude_poll_votes(opts)
@@ -1697,9 +1707,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
Fetcher.fetch_and_contain_remote_object_from_id(first) do
{:ok, false}
else
- {:error, {:ok, %{status: code}}} when code in [401, 403] -> {:ok, true}
- {:error, _} = e -> e
- e -> {:error, e}
+ {:error, _} -> {:ok, true}
end
end
diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex
@@ -273,12 +273,17 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
end
def inbox(%{assigns: %{valid_signature: true}} = conn, %{"nickname" => nickname} = params) do
- with %User{} = recipient <- User.get_cached_by_nickname(nickname),
- {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(params["actor"]),
+ with %User{is_active: true} = recipient <- User.get_cached_by_nickname(nickname),
+ {:ok, %User{is_active: true} = actor} <- User.get_or_fetch_by_ap_id(params["actor"]),
true <- Utils.recipient_in_message(recipient, actor, params),
params <- Utils.maybe_splice_recipient(recipient.ap_id, params) do
Federator.incoming_ap_doc(params)
json(conn, "ok")
+ else
+ _ ->
+ conn
+ |> put_status(:bad_request)
+ |> json("Invalid request.")
end
end
diff --git a/lib/pleroma/web/activity_pub/builder.ex b/lib/pleroma/web/activity_pub/builder.ex
@@ -9,6 +9,7 @@ defmodule Pleroma.Web.ActivityPub.Builder do
This module encodes our addressing policies and general shape of our objects.
"""
+ alias Pleroma.Activity
alias Pleroma.Emoji
alias Pleroma.Object
alias Pleroma.User
@@ -131,7 +132,7 @@ defmodule Pleroma.Web.ActivityPub.Builder do
def emoji_react(actor, object, emoji) do
with {:ok, data, meta} <- object_action(actor, object) do
data =
- if Emoji.is_unicode_emoji?(emoji) do
+ if Emoji.unicode?(emoji) do
unicode_emoji_react(object, data, emoji)
else
custom_emoji_react(object, data, emoji)
@@ -347,7 +348,7 @@ defmodule Pleroma.Web.ActivityPub.Builder do
actor.ap_id == Relay.ap_id() ->
[actor.follower_address]
- public? and Visibility.is_local_public?(object) ->
+ public? and Visibility.local_public?(object) ->
[actor.follower_address, object.data["actor"], Utils.as_local_public()]
public? ->
@@ -375,7 +376,7 @@ defmodule Pleroma.Web.ActivityPub.Builder do
# Address the actor of the object, and our actor's follower collection if the post is public.
to =
- if Visibility.is_public?(object) do
+ if Visibility.public?(object) do
[actor.follower_address, object.data["actor"]]
else
[object.data["actor"]]
diff --git a/lib/pleroma/web/activity_pub/mrf.ex b/lib/pleroma/web/activity_pub/mrf.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF do
@@ -139,7 +139,16 @@ defmodule Pleroma.Web.ActivityPub.MRF do
@spec subdomains_regex([String.t()]) :: [Regex.t()]
def subdomains_regex(domains) when is_list(domains) do
- for domain <- domains, do: ~r(^#{String.replace(domain, "*.", "(.*\\.)*")}$)i
+ for domain <- domains do
+ try do
+ target = String.replace(domain, "*.", "(.*\\.)*")
+ ~r<^#{target}$>i
+ rescue
+ e ->
+ Logger.error("MRF: Invalid subdomain Regex: #{domain}")
+ reraise e, __STACKTRACE__
+ end
+ end
end
@spec subdomain_match?([Regex.t()], String.t()) :: boolean()
diff --git a/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex b/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex
@@ -56,8 +56,6 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy do
nick_score + name_score + actor_type_score
end
- defp determine_if_followbot(_), do: 0.0
-
defp bot_allowed?(%{"object" => target}, bot_actor) do
%User{} = user = normalize_by_ap_id(target)
diff --git a/lib/pleroma/web/activity_pub/mrf/force_mention.ex b/lib/pleroma/web/activity_pub/mrf/force_mention.ex
@@ -0,0 +1,59 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.MRF.ForceMention do
+ require Pleroma.Constants
+
+ alias Pleroma.Config
+ alias Pleroma.Object
+ alias Pleroma.User
+
+ @behaviour Pleroma.Web.ActivityPub.MRF.Policy
+
+ defp get_author(url) do
+ with %Object{data: %{"actor" => actor}} <- Object.normalize(url, fetch: false),
+ %User{ap_id: ap_id, nickname: nickname} <- User.get_cached_by_ap_id(actor) do
+ %{"type" => "Mention", "href" => ap_id, "name" => "@#{nickname}"}
+ else
+ _ -> nil
+ end
+ end
+
+ defp prepend_author(tags, _, false), do: tags
+
+ defp prepend_author(tags, nil, _), do: tags
+
+ defp prepend_author(tags, url, _) do
+ actor = get_author(url)
+
+ if not is_nil(actor) do
+ [actor | tags]
+ else
+ tags
+ end
+ end
+
+ @impl true
+ def filter(%{"type" => "Create", "object" => %{"tag" => tag} = object} = activity) do
+ tag =
+ tag
+ |> prepend_author(
+ object["inReplyTo"],
+ Config.get([:mrf_force_mention, :mention_parent, true])
+ )
+ |> prepend_author(
+ object["quoteUrl"],
+ Config.get([:mrf_force_mention, :mention_quoted, true])
+ )
+ |> Enum.uniq()
+
+ {:ok, put_in(activity["object"]["tag"], tag)}
+ end
+
+ @impl true
+ def filter(object), do: {:ok, object}
+
+ @impl true
+ def describe, do: {:ok, %{}}
+end
diff --git a/lib/pleroma/web/activity_pub/mrf/hashtag_policy.ex b/lib/pleroma/web/activity_pub/mrf/hashtag_policy.ex
@@ -9,7 +9,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.HashtagPolicy do
alias Pleroma.Object
@moduledoc """
- Reject, TWKN-remove or Set-Sensitive messsages with specific hashtags (without the leading #)
+ Reject, TWKN-remove or Set-Sensitive messages with specific hashtags (without the leading #)
Note: This MRF Policy is always enabled, if you want to disable it you have to set empty lists.
"""
@@ -84,7 +84,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.HashtagPolicy do
if hashtags != [] do
with {:ok, message} <- check_reject(message, hashtags),
{:ok, message} <-
- (if "type" == "Create" do
+ (if type == "Create" do
check_ftl_removal(message, hashtags)
else
{:ok, message}
diff --git a/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex b/lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex
@@ -62,7 +62,6 @@ defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy do
key: :mrf_inline_quote,
related_policy: "Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy",
label: "MRF Inline Quote Policy",
- type: :group,
description: "Force quote url to appear in post content.",
children: [
%{
diff --git a/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex b/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex
@@ -10,15 +10,12 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do
@moduledoc "Reject or Word-Replace messages with a keyword or regex"
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
- defp string_matches?(string, _) when not is_binary(string) do
- false
- end
defp string_matches?(string, pattern) when is_binary(pattern) do
String.contains?(string, pattern)
end
- defp string_matches?(string, pattern) do
+ defp string_matches?(string, %Regex{} = pattern) do
String.match?(string, pattern)
end
diff --git a/lib/pleroma/web/activity_pub/mrf/no_empty_policy.ex b/lib/pleroma/web/activity_pub/mrf/no_empty_policy.ex
@@ -10,9 +10,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.NoEmptyPolicy do
@impl true
def filter(%{"actor" => actor} = object) do
- with true <- is_local?(actor),
- true <- is_eligible_type?(object),
- true <- is_note?(object),
+ with true <- local?(actor),
+ true <- eligible_type?(object),
+ true <- note?(object),
false <- has_attachment?(object),
true <- only_mentions?(object) do
{:reject, "[NoEmptyPolicy]"}
@@ -24,7 +24,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.NoEmptyPolicy do
def filter(object), do: {:ok, object}
- defp is_local?(actor) do
+ defp local?(actor) do
if actor |> String.starts_with?("#{Endpoint.url()}") do
true
else
@@ -59,11 +59,11 @@ defmodule Pleroma.Web.ActivityPub.MRF.NoEmptyPolicy do
defp only_mentions?(_), do: false
- defp is_note?(%{"object" => %{"type" => "Note"}}), do: true
- defp is_note?(_), do: false
+ defp note?(%{"object" => %{"type" => "Note"}}), do: true
+ defp note?(_), do: false
- defp is_eligible_type?(%{"type" => type}) when type in ["Create", "Update"], do: true
- defp is_eligible_type?(_), do: false
+ defp eligible_type?(%{"type" => type}) when type in ["Create", "Update"], do: true
+ defp eligible_type?(_), do: false
@impl true
def describe, do: {:ok, %{}}
diff --git a/lib/pleroma/web/activity_pub/mrf/policy.ex b/lib/pleroma/web/activity_pub/mrf/policy.ex
@@ -3,8 +3,8 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.Policy do
- @callback filter(Map.t()) :: {:ok | :reject, Map.t()}
- @callback describe() :: {:ok | :error, Map.t()}
+ @callback filter(map()) :: {:ok | :reject, map()}
+ @callback describe() :: {:ok | :error, map()}
@callback config_description() :: %{
optional(:children) => [map()],
key: atom(),
diff --git a/lib/pleroma/web/activity_pub/mrf/quote_to_link_tag_policy.ex b/lib/pleroma/web/activity_pub/mrf/quote_to_link_tag_policy.ex
@@ -28,7 +28,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.QuoteToLinkTagPolicy do
tags = object["tag"] || []
if Enum.any?(tags, fn tag ->
- CommonFixes.is_object_link_tag(tag) and tag["href"] == quote_url
+ CommonFixes.object_link_tag?(tag) and tag["href"] == quote_url
end) do
object
else
diff --git a/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex b/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex
@@ -34,7 +34,10 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy do
|> Path.basename()
|> Path.extname()
- file_path = Path.join(emoji_dir_path, shortcode <> (extension || ".png"))
+ extension = if extension == "", do: ".png", else: extension
+
+ shortcode = Path.basename(shortcode)
+ file_path = Path.join(emoji_dir_path, shortcode <> extension)
case File.write(file_path, response.body) do
:ok ->
@@ -76,6 +79,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy do
new_emojis =
foreign_emojis
|> Enum.reject(fn {shortcode, _url} -> shortcode in installed_emoji end)
+ |> Enum.reject(fn {shortcode, _url} -> String.contains?(shortcode, ["/", "\\"]) end)
|> Enum.filter(fn {shortcode, _url} ->
reject_emoji? =
[:mrf_steal_emoji, :rejected_shortcodes]
diff --git a/lib/pleroma/web/activity_pub/object_validator.ex b/lib/pleroma/web/activity_pub/object_validator.ex
@@ -173,6 +173,9 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
{:object_validation, e} ->
e
+
+ {:error, %Ecto.Changeset{} = e} ->
+ {:error, e}
end
end
diff --git a/lib/pleroma/web/activity_pub/object_validators/announce_validator.ex b/lib/pleroma/web/activity_pub/object_validators/announce_validator.ex
@@ -82,7 +82,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidator do
object when is_binary(object) <- get_field(cng, :object),
%User{} = actor <- User.get_cached_by_ap_id(actor),
%Object{} = object <- Object.get_cached_by_ap_id(object),
- false <- Visibility.is_public?(object) do
+ false <- Visibility.public?(object) do
same_actor = object.data["actor"] == actor.ap_id
recipients = get_field(cng, :to) ++ get_field(cng, :cc)
local_public = Utils.as_local_public()
diff --git a/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex b/lib/pleroma/web/activity_pub/object_validators/attachment_validator.ex
@@ -12,13 +12,13 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidator do
@primary_key false
embedded_schema do
field(:id, :string)
- field(:type, :string)
+ field(:type, :string, default: "Link")
field(:mediaType, ObjectValidators.MIME, default: "application/octet-stream")
field(:name, :string)
field(:blurhash, :string)
embeds_many :url, UrlObjectValidator, primary_key: false do
- field(:type, :string)
+ field(:type, :string, default: "Link")
field(:href, ObjectValidators.Uri)
field(:mediaType, ObjectValidators.MIME, default: "application/octet-stream")
field(:width, :integer)
diff --git a/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex b/lib/pleroma/web/activity_pub/object_validators/chat_message_validator.ex
@@ -57,6 +57,11 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator do
|> Map.put("attachment", attachment)
end
+ def fix_attachment(%{"attachment" => attachment} = data) when attachment == [] do
+ data
+ |> Map.drop(["attachment"])
+ end
+
def fix_attachment(data), do: data
def changeset(struct, data) do
diff --git a/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex b/lib/pleroma/web/activity_pub/object_validators/common_fixes.ex
@@ -4,6 +4,7 @@
defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do
alias Pleroma.EctoType.ActivityPub.ObjectValidators
+ alias Pleroma.Maps
alias Pleroma.Object
alias Pleroma.Object.Containment
alias Pleroma.User
@@ -24,6 +25,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do
end
def fix_object_defaults(data) do
+ data = Maps.filter_empty_values(data)
+
context =
Utils.maybe_create_context(
data["context"] || data["conversation"] || data["inReplyTo"] || data["id"]
@@ -99,7 +102,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do
end
def fix_quote_url(%{"tag" => [_ | _] = tags} = data) do
- tag = Enum.find(tags, &is_object_link_tag/1)
+ tag = Enum.find(tags, &object_link_tag?/1)
if not is_nil(tag) do
data
@@ -112,7 +115,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do
def fix_quote_url(data), do: data
# https://codeberg.org/fediverse/fep/src/branch/main/fep/e232/fep-e232.md
- def is_object_link_tag(%{
+ def object_link_tag?(%{
"type" => "Link",
"mediaType" => media_type,
"href" => href
@@ -121,5 +124,5 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do
true
end
- def is_object_link_tag(_), do: false
+ def object_link_tag?(_), do: false
end
diff --git a/lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex b/lib/pleroma/web/activity_pub/object_validators/emoji_react_validator.ex
@@ -74,10 +74,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do
new_emoji = Pleroma.Emoji.fully_qualify_emoji(emoji)
cond do
- Pleroma.Emoji.is_unicode_emoji?(emoji) ->
+ Pleroma.Emoji.unicode?(emoji) ->
data
- Pleroma.Emoji.is_unicode_emoji?(new_emoji) ->
+ Pleroma.Emoji.unicode?(new_emoji) ->
data |> Map.put("content", new_emoji)
true ->
@@ -90,7 +90,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do
defp validate_emoji(cng) do
content = get_field(cng, :content)
- if Emoji.is_unicode_emoji?(content) || Emoji.is_custom_emoji?(content) do
+ if Emoji.unicode?(content) || Emoji.custom?(content) do
cng
else
cng
@@ -101,7 +101,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do
defp maybe_validate_tag_presence(cng) do
content = get_field(cng, :content)
- if Emoji.is_unicode_emoji?(content) do
+ if Emoji.unicode?(content) do
cng
else
tag = get_field(cng, :tag)
diff --git a/lib/pleroma/web/activity_pub/object_validators/question_options_validator.ex b/lib/pleroma/web/activity_pub/object_validators/question_options_validator.ex
@@ -14,10 +14,10 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionOptionsValidator do
embeds_one :replies, Replies, primary_key: false do
field(:totalItems, :integer)
- field(:type, :string)
+ field(:type, :string, default: "Collection")
end
- field(:type, :string)
+ field(:type, :string, default: "Note")
end
def changeset(struct, data) do
diff --git a/lib/pleroma/web/activity_pub/object_validators/question_validator.ex b/lib/pleroma/web/activity_pub/object_validators/question_validator.ex
@@ -29,6 +29,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator do
field(:closed, ObjectValidators.DateTime)
field(:voters, {:array, ObjectValidators.ObjectID}, default: [])
+ field(:nonAnonymous, :boolean)
embeds_many(:anyOf, QuestionOptionsValidator)
embeds_many(:oneOf, QuestionOptionsValidator)
end
diff --git a/lib/pleroma/web/activity_pub/pipeline.ex b/lib/pleroma/web/activity_pub/pipeline.ex
@@ -62,7 +62,7 @@ defmodule Pleroma.Web.ActivityPub.Pipeline do
with {:ok, local} <- Keyword.fetch(meta, :local) do
do_not_federate = meta[:do_not_federate] || !config().get([:instance, :federating])
- if !do_not_federate and local and not Visibility.is_local_public?(activity) do
+ if !do_not_federate and local and not Visibility.local_public?(activity) do
activity =
if object = Keyword.get(meta, :object_data) do
%{activity | data: Map.put(activity.data, "object", object)}
diff --git a/lib/pleroma/web/activity_pub/publisher.ex b/lib/pleroma/web/activity_pub/publisher.ex
@@ -13,13 +13,12 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.ActivityPub.Transmogrifier
+ alias Pleroma.Workers.PublisherWorker
require Pleroma.Constants
import Pleroma.Web.ActivityPub.Visibility
- @behaviour Pleroma.Web.Federator.Publisher
-
require Logger
@moduledoc """
@@ -27,9 +26,47 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
"""
@doc """
+ Enqueue publishing a single activity.
+ """
+ @spec enqueue_one(map(), Keyword.t()) :: {:ok, %Oban.Job{}}
+ def enqueue_one(%{} = params, worker_args \\ []) do
+ PublisherWorker.enqueue(
+ "publish_one",
+ %{"params" => params},
+ worker_args
+ )
+ end
+
+ @doc """
+ Gathers a set of remote users given an IR envelope.
+ """
+ def remote_users(%User{id: user_id}, %{data: %{"to" => to} = data}) do
+ cc = Map.get(data, "cc", [])
+
+ bcc =
+ data
+ |> Map.get("bcc", [])
+ |> Enum.reduce([], fn ap_id, bcc ->
+ case Pleroma.List.get_by_ap_id(ap_id) do
+ %Pleroma.List{user_id: ^user_id} = list ->
+ {:ok, following} = Pleroma.List.get_following(list)
+ bcc ++ Enum.map(following, & &1.ap_id)
+
+ _ ->
+ bcc
+ end
+ end)
+
+ [to, cc, bcc]
+ |> Enum.concat()
+ |> Enum.map(&User.get_cached_by_ap_id/1)
+ |> Enum.filter(fn user -> user && !user.local end)
+ end
+
+ @doc """
Determine if an activity can be represented by running it through Transmogrifier.
"""
- def is_representable?(%Activity{} = activity) do
+ def representable?(%Activity{} = activity) do
with {:ok, _data} <- Transmogrifier.prepare_outgoing(activity.data) do
true
else
@@ -80,9 +117,27 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
result
else
- {_post_result, response} ->
+ {_post_result, %{status: code} = response} = e ->
unless params[:unreachable_since], do: Instances.set_unreachable(inbox)
- {:error, response}
+ Logger.metadata(activity: id, inbox: inbox, status: code)
+ Logger.error("Publisher failed to inbox #{inbox} with status #{code}")
+
+ case response do
+ %{status: 403} -> {:discard, :forbidden}
+ %{status: 404} -> {:discard, :not_found}
+ %{status: 410} -> {:discard, :not_found}
+ _ -> {:error, e}
+ end
+
+ {:error, :pool_full} ->
+ Logger.debug("Publisher snoozing worker job due to full connection pool")
+ {:snooze, 30}
+
+ e ->
+ unless params[:unreachable_since], do: Instances.set_unreachable(inbox)
+ Logger.metadata(activity: id, inbox: inbox)
+ Logger.error("Publisher failed to inbox #{inbox} #{inspect(e)}")
+ {:error, e}
end
end
@@ -103,19 +158,18 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
end
end
- defp should_federate?(inbox, public) do
- if public do
- true
- else
- %{host: host} = URI.parse(inbox)
+ def should_federate?(nil, _), do: false
+ def should_federate?(_, true), do: true
- quarantined_instances =
- Config.get([:instance, :quarantined_instances], [])
- |> Pleroma.Web.ActivityPub.MRF.instance_list_from_tuples()
- |> Pleroma.Web.ActivityPub.MRF.subdomains_regex()
+ def should_federate?(inbox, _) do
+ %{host: host} = URI.parse(inbox)
- !Pleroma.Web.ActivityPub.MRF.subdomain_match?(quarantined_instances, host)
- end
+ quarantined_instances =
+ Config.get([:instance, :quarantined_instances], [])
+ |> Pleroma.Web.ActivityPub.MRF.instance_list_from_tuples()
+ |> Pleroma.Web.ActivityPub.MRF.subdomains_regex()
+
+ !Pleroma.Web.ActivityPub.MRF.subdomain_match?(quarantined_instances, host)
end
@spec recipients(User.t(), Activity.t()) :: [[User.t()]]
@@ -138,7 +192,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
[]
end
- mentioned = Pleroma.Web.Federator.Publisher.remote_users(actor, activity)
+ mentioned = remote_users(actor, activity)
non_mentioned = (followers ++ fetchers) -- mentioned
[mentioned, non_mentioned]
@@ -195,7 +249,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
def publish(%User{} = actor, %{data: %{"bcc" => bcc}} = activity)
when is_list(bcc) and bcc != [] do
- public = is_public?(activity)
+ public = public?(activity)
{:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
[priority_recipients, recipients] = recipients(actor, activity)
@@ -204,7 +258,10 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
[priority_recipients, recipients]
|> Enum.map(fn recipients ->
recipients
- |> Enum.map(fn actor -> actor.inbox end)
+ |> Enum.map(fn %User{} = user ->
+ determine_inbox(activity, user)
+ end)
+ |> Enum.uniq()
|> Enum.filter(fn inbox -> should_federate?(inbox, public) end)
|> Instances.filter_reachable()
end)
@@ -223,7 +280,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
|> Map.put("cc", cc)
|> Jason.encode!()
- Pleroma.Web.Federator.Publisher.enqueue_one(__MODULE__, %{
+ __MODULE__.enqueue_one(%{
inbox: inbox,
json: json,
actor_id: actor.id,
@@ -237,7 +294,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
# Publishes an activity to all relevant peers.
def publish(%User{} = actor, %Activity{} = activity) do
- public = is_public?(activity)
+ public = public?(activity)
if public && Config.get([:instance, :allow_relay]) do
Logger.debug(fn -> "Relaying #{activity.data["id"]} out" end)
@@ -251,7 +308,10 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
recipients(actor, activity)
|> Enum.map(fn recipients ->
recipients
- |> Enum.map(fn actor -> actor.inbox end)
+ |> Enum.map(fn %User{} = user ->
+ determine_inbox(activity, user)
+ end)
+ |> Enum.uniq()
|> Enum.filter(fn inbox -> should_federate?(inbox, public) end)
end)
@@ -262,8 +322,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
inboxes
|> Instances.filter_reachable()
|> Enum.each(fn {inbox, unreachable_since} ->
- Pleroma.Web.Federator.Publisher.enqueue_one(
- __MODULE__,
+ __MODULE__.enqueue_one(
%{
inbox: inbox,
json: json,
diff --git a/lib/pleroma/web/activity_pub/relay.ex b/lib/pleroma/web/activity_pub/relay.ex
@@ -58,7 +58,7 @@ defmodule Pleroma.Web.ActivityPub.Relay do
@spec publish(any()) :: {:ok, Activity.t()} | {:error, any()}
def publish(%Activity{data: %{"type" => "Create"}} = activity) do
with %User{} = user <- get_actor(),
- true <- Visibility.is_public?(activity) do
+ true <- Visibility.public?(activity) do
CommonAPI.repeat(activity.id, user)
else
error -> format_error(error)
diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex
@@ -21,7 +21,6 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Utils
- alias Pleroma.Web.Push
alias Pleroma.Web.Streamer
alias Pleroma.Workers.PollWorker
@@ -125,7 +124,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
nil
end
- {:ok, notifications} = Notification.create_notifications(object, do_send: false)
+ {:ok, notifications} = Notification.create_notifications(object)
meta =
meta
@@ -184,7 +183,11 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
liked_object = Object.get_by_ap_id(object.data["object"])
Utils.add_like_to_object(object, liked_object)
- Notification.create_notifications(object)
+ {:ok, notifications} = Notification.create_notifications(object)
+
+ meta =
+ meta
+ |> add_notifications(notifications)
{:ok, object, meta}
end
@@ -202,7 +205,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
def handle(%{data: %{"type" => "Create"}} = activity, meta) do
with {:ok, object, meta} <- handle_object_creation(meta[:object_data], activity, meta),
%User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
- {:ok, notifications} = Notification.create_notifications(activity, do_send: false)
+ {:ok, notifications} = Notification.create_notifications(activity)
{:ok, _user} = ActivityPub.increase_note_count_if_public(user, object)
{:ok, _user} = ActivityPub.update_last_status_at_if_public(user, object)
@@ -227,12 +230,12 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
end
end
- ConcurrentLimiter.limit(Pleroma.Web.RichMedia.Helpers, fn ->
- Task.start(fn -> Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity) end)
- end)
+ Pleroma.Web.RichMedia.Card.get_by_activity(activity)
Pleroma.Search.add_to_index(Map.put(activity, :object, object))
+ Utils.maybe_handle_group_posts(activity)
+
meta =
meta
|> add_notifications(notifications)
@@ -256,11 +259,13 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
Utils.add_announce_to_object(object, announced_object)
- if !User.is_internal_user?(user) do
- Notification.create_notifications(object)
+ {:ok, notifications} = Notification.create_notifications(object)
- ap_streamer().stream_out(object)
- end
+ if !User.internal?(user), do: ap_streamer().stream_out(object)
+
+ meta =
+ meta
+ |> add_notifications(notifications)
{:ok, object, meta}
end
@@ -281,7 +286,11 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
reacted_object = Object.get_by_ap_id(object.data["object"])
Utils.add_emoji_reaction_to_object(object, reacted_object)
- Notification.create_notifications(object)
+ {:ok, notifications} = Notification.create_notifications(object)
+
+ meta =
+ meta
+ |> add_notifications(notifications)
{:ok, object, meta}
end
@@ -302,9 +311,9 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
result =
case deleted_object do
%Object{} ->
- with {:ok, deleted_object, _activity} <- Object.delete(deleted_object),
+ with {_, {:ok, deleted_object, _activity}} <- {:object, Object.delete(deleted_object)},
{_, actor} when is_binary(actor) <- {:actor, deleted_object.data["actor"]},
- %User{} = user <- User.get_cached_by_ap_id(actor) do
+ {_, %User{} = user} <- {:user, User.get_cached_by_ap_id(actor)} do
User.remove_pinned_object_id(user, deleted_object.data["id"])
{:ok, user} = ActivityPub.decrease_note_count_if_public(user, deleted_object)
@@ -326,6 +335,17 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
{:actor, _} ->
@logger.error("The object doesn't have an actor: #{inspect(deleted_object)}")
:no_object_actor
+
+ {:user, _} ->
+ @logger.error(
+ "The object's actor could not be resolved to a user: #{inspect(deleted_object)}"
+ )
+
+ :no_object_user
+
+ {:object, _} ->
+ @logger.error("The object could not be deleted: #{inspect(deleted_object)}")
+ {:error, object}
end
%User{} ->
@@ -567,17 +587,14 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
def handle_undoing(object), do: {:error, ["don't know how to handle", object]}
- @spec delete_object(Object.t()) :: :ok | {:error, Ecto.Changeset.t()}
+ @spec delete_object(Activity.t()) :: :ok | {:error, Ecto.Changeset.t()}
defp delete_object(object) do
with {:ok, _} <- Repo.delete(object), do: :ok
end
defp send_notifications(meta) do
Keyword.get(meta, :notifications, [])
- |> Enum.each(fn notification ->
- Streamer.stream(["user", "user:notification"], notification)
- Push.send(notification)
- end)
+ |> Notification.send()
meta
end
diff --git a/lib/pleroma/web/activity_pub/side_effects/handling.ex b/lib/pleroma/web/activity_pub/side_effects/handling.ex
@@ -4,5 +4,5 @@
defmodule Pleroma.Web.ActivityPub.SideEffects.Handling do
@callback handle(map(), keyword()) :: {:ok, map(), keyword()} | {:error, any()}
- @callback handle_after_transaction(map()) :: map()
+ @callback handle_after_transaction(keyword()) :: keyword()
end
diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex
@@ -23,7 +23,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
import Ecto.Query
- require Logger
require Pleroma.Constants
@doc """
@@ -155,8 +154,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> Map.put("context", replied_object.data["context"] || object["conversation"])
|> Map.drop(["conversation", "inReplyToAtomUri"])
else
- e ->
- Logger.warning("Couldn't fetch #{inspect(in_reply_to_id)}, error: #{inspect(e)}")
+ _ ->
object
end
else
@@ -181,8 +179,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
{:quoting?, _} ->
object
- e ->
- Logger.warning("Couldn't fetch #{inspect(quote_url)}, error: #{inspect(e)}")
+ _ ->
object
end
end
@@ -782,7 +779,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> Object.normalize(fetch: false)
data =
- if Visibility.is_private?(object) && object.data["actor"] == ap_id do
+ if Visibility.private?(object) && object.data["actor"] == ap_id do
data |> Map.put("object", object |> Map.get(:data) |> prepare_object)
else
data |> maybe_fix_object_url
@@ -852,8 +849,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
relative_object do
Map.put(data, "object", external_url)
else
- {:fetch, e} ->
- Logger.error("Couldn't fetch #{object} #{inspect(e)}")
+ {:fetch, _} ->
data
_ ->
diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex
@@ -167,7 +167,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
with true <- Config.get!([:instance, :federating]),
true <- type != "Block" || outgoing_blocks,
- false <- Visibility.is_local_public?(activity) do
+ false <- Visibility.local_public?(activity) do
Pleroma.Web.Federator.publish(activity)
end
@@ -277,7 +277,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
object_actor = User.get_cached_by_ap_id(object_actor_id)
to =
- if Visibility.is_public?(object) do
+ if Visibility.public?(object) do
[actor.follower_address, object.data["actor"]]
else
[object.data["actor"]]
@@ -721,14 +721,18 @@ defmodule Pleroma.Web.ActivityPub.Utils do
#### Flag-related helpers
@spec make_flag_data(map(), map()) :: map()
- def make_flag_data(%{actor: actor, context: context, content: content} = params, additional) do
+ def make_flag_data(
+ %{actor: actor, context: context, content: content} = params,
+ additional
+ ) do
%{
"type" => "Flag",
"actor" => actor.ap_id,
"content" => content,
"object" => build_flag_object(params),
"context" => context,
- "state" => "open"
+ "state" => "open",
+ "rules" => Map.get(params, :rules, nil)
}
|> Map.merge(additional)
end
@@ -776,10 +780,9 @@ defmodule Pleroma.Web.ActivityPub.Utils do
build_flag_object(object)
nil ->
- if %Object{} = object = Object.get_by_ap_id(id) do
- build_flag_object(object)
- else
- %{"id" => id, "deleted" => true}
+ case Object.get_by_ap_id(id) do
+ %Object{} = object -> build_flag_object(object)
+ _ -> %{"id" => id, "deleted" => true}
end
end
end
@@ -935,4 +938,27 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|> where([a, object: o], fragment("(?)->>'type' = 'Answer'", o.data))
|> Repo.all()
end
+
+ def maybe_handle_group_posts(activity) do
+ poster = User.get_cached_by_ap_id(activity.actor)
+
+ mentions =
+ activity.data["to"]
+ |> Enum.filter(&(&1 != activity.actor))
+
+ mentioned_local_groups =
+ User.get_all_by_ap_id(mentions)
+ |> Enum.filter(fn user ->
+ user.actor_type == "Group" and
+ user.local and
+ not User.blocks?(user, poster)
+ end)
+
+ mentioned_local_groups
+ |> Enum.each(fn group ->
+ Pleroma.Web.CommonAPI.repeat(activity.id, group)
+ end)
+
+ :ok
+ end
end
diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex
@@ -67,8 +67,13 @@ defmodule Pleroma.Web.ActivityPub.UserView do
def render("user.json", %{user: %User{nickname: nil} = user}),
do: render("service.json", %{user: user})
- def render("user.json", %{user: %User{nickname: "internal." <> _} = user}),
- do: render("service.json", %{user: user}) |> Map.put("preferredUsername", user.nickname)
+ def render("user.json", %{user: %User{nickname: "internal." <> _} = user}) do
+ render("service.json", %{user: user})
+ |> Map.merge(%{
+ "preferredUsername" => user.nickname,
+ "webfinger" => "acct:#{User.full_nickname(user)}"
+ })
+ end
def render("user.json", %{user: user}) do
{:ok, _, public_key} = Keys.keys_from_pem(user.keys)
@@ -121,7 +126,8 @@ defmodule Pleroma.Web.ActivityPub.UserView do
"discoverable" => user.is_discoverable,
"capabilities" => capabilities,
"alsoKnownAs" => user.also_known_as,
- "vcard:bday" => birthday
+ "vcard:bday" => birthday,
+ "webfinger" => "acct:#{User.full_nickname(user)}"
}
|> Map.merge(maybe_make_image(&User.avatar_url/2, "icon", user))
|> Map.merge(maybe_make_image(&User.banner_url/2, "image", user))
diff --git a/lib/pleroma/web/activity_pub/visibility.ex b/lib/pleroma/web/activity_pub/visibility.ex
@@ -11,28 +11,28 @@ defmodule Pleroma.Web.ActivityPub.Visibility do
require Pleroma.Constants
- @spec is_public?(Object.t() | Activity.t() | map()) :: boolean()
- def is_public?(%Object{data: %{"type" => "Tombstone"}}), do: false
- def is_public?(%Object{data: data}), do: is_public?(data)
- def is_public?(%Activity{data: %{"type" => "Move"}}), do: true
- def is_public?(%Activity{data: data}), do: is_public?(data)
- def is_public?(%{"directMessage" => true}), do: false
-
- def is_public?(data) do
+ @spec public?(Object.t() | Activity.t() | map()) :: boolean()
+ def public?(%Object{data: %{"type" => "Tombstone"}}), do: false
+ def public?(%Object{data: data}), do: public?(data)
+ def public?(%Activity{data: %{"type" => "Move"}}), do: true
+ def public?(%Activity{data: data}), do: public?(data)
+ def public?(%{"directMessage" => true}), do: false
+
+ def public?(data) do
Utils.label_in_message?(Pleroma.Constants.as_public(), data) or
Utils.label_in_message?(Utils.as_local_public(), data)
end
- def is_local_public?(%Object{data: data}), do: is_local_public?(data)
- def is_local_public?(%Activity{data: data}), do: is_local_public?(data)
+ def local_public?(%Object{data: data}), do: local_public?(data)
+ def local_public?(%Activity{data: data}), do: local_public?(data)
- def is_local_public?(data) do
+ def local_public?(data) do
Utils.label_in_message?(Utils.as_local_public(), data) and
not Utils.label_in_message?(Pleroma.Constants.as_public(), data)
end
- def is_private?(activity) do
- with false <- is_public?(activity),
+ def private?(activity) do
+ with false <- public?(activity),
%User{follower_address: follower_address} <-
User.get_cached_by_ap_id(activity.data["actor"]) do
follower_address in activity.data["to"]
@@ -41,20 +41,20 @@ defmodule Pleroma.Web.ActivityPub.Visibility do
end
end
- def is_announceable?(activity, user, public \\ true) do
- is_public?(activity) ||
- (!public && is_private?(activity) && activity.data["actor"] == user.ap_id)
+ def announceable?(activity, user, public \\ true) do
+ public?(activity) ||
+ (!public && private?(activity) && activity.data["actor"] == user.ap_id)
end
- def is_direct?(%Activity{data: %{"directMessage" => true}}), do: true
- def is_direct?(%Object{data: %{"directMessage" => true}}), do: true
+ def direct?(%Activity{data: %{"directMessage" => true}}), do: true
+ def direct?(%Object{data: %{"directMessage" => true}}), do: true
- def is_direct?(activity) do
- !is_public?(activity) && !is_private?(activity)
+ def direct?(activity) do
+ !public?(activity) && !private?(activity)
end
- def is_list?(%{data: %{"listMessage" => _}}), do: true
- def is_list?(_), do: false
+ def list?(%{data: %{"listMessage" => _}}), do: true
+ def list?(_), do: false
@spec visible_for_user?(Object.t() | Activity.t() | nil, User.t() | nil) :: boolean()
def visible_for_user?(%Object{data: %{"type" => "Tombstone"}}, _), do: false
@@ -77,7 +77,7 @@ defmodule Pleroma.Web.ActivityPub.Visibility do
when module in [Activity, Object] do
if restrict_unauthenticated_access?(message),
do: false,
- else: is_public?(message) and not is_local_public?(message)
+ else: public?(message) and not local_public?(message)
end
def visible_for_user?(%{__struct__: module} = message, user)
@@ -86,8 +86,8 @@ defmodule Pleroma.Web.ActivityPub.Visibility do
y = [message.data["actor"]] ++ message.data["to"] ++ (message.data["cc"] || [])
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)
+ federatable = not local_public?(message)
+ (public?(message) || Enum.any?(x, &(&1 in y))) and (user_is_local || federatable)
end
def entire_thread_visible_for_user?(%Activity{} = activity, %User{} = user) do
diff --git a/lib/pleroma/web/admin_api/controllers/config_controller.ex b/lib/pleroma/web/admin_api/controllers/config_controller.ex
@@ -9,7 +9,7 @@ defmodule Pleroma.Web.AdminAPI.ConfigController do
alias Pleroma.ConfigDB
alias Pleroma.Web.Plugs.OAuthScopesPlug
- plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
plug(OAuthScopesPlug, %{scopes: ["admin:write"]} when action == :update)
plug(
@@ -76,7 +76,7 @@ defmodule Pleroma.Web.AdminAPI.ConfigController do
json(conn, translate_descriptions(descriptions))
end
- def show(conn, %{only_db: true}) do
+ def show(%{private: %{open_api_spex: %{params: %{only_db: true}}}} = conn, _) do
with :ok <- configurable_from_database() do
configs = Pleroma.Repo.all(ConfigDB)
@@ -128,7 +128,7 @@ defmodule Pleroma.Web.AdminAPI.ConfigController do
end
end
- def update(%{body_params: %{configs: configs}} = conn, _) do
+ def update(%{private: %{open_api_spex: %{body_params: %{configs: configs}}}} = conn, _) do
with :ok <- configurable_from_database() do
results =
configs
diff --git a/lib/pleroma/web/admin_api/controllers/instance_document_controller.ex b/lib/pleroma/web/admin_api/controllers/instance_document_controller.ex
@@ -9,7 +9,7 @@ defmodule Pleroma.Web.AdminAPI.InstanceDocumentController do
alias Pleroma.Web.Plugs.InstanceStatic
alias Pleroma.Web.Plugs.OAuthScopesPlug
- plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
action_fallback(Pleroma.Web.AdminAPI.FallbackController)
@@ -18,7 +18,7 @@ defmodule Pleroma.Web.AdminAPI.InstanceDocumentController do
plug(OAuthScopesPlug, %{scopes: ["admin:read"]} when action == :show)
plug(OAuthScopesPlug, %{scopes: ["admin:write"]} when action in [:update, :delete])
- def show(conn, %{name: document_name}) do
+ def show(%{private: %{open_api_spex: %{params: %{name: document_name}}}} = conn, _) do
with {:ok, url} <- InstanceDocument.get(document_name),
{:ok, content} <- File.read(InstanceStatic.file_path(url)) do
conn
@@ -27,13 +27,18 @@ defmodule Pleroma.Web.AdminAPI.InstanceDocumentController do
end
end
- def update(%{body_params: %{file: file}} = conn, %{name: document_name}) do
+ def update(
+ %{
+ private: %{open_api_spex: %{body_params: %{file: file}, params: %{name: document_name}}}
+ } = conn,
+ _
+ ) do
with {:ok, url} <- InstanceDocument.put(document_name, file.path) do
json(conn, %{"url" => url})
end
end
- def delete(conn, %{name: document_name}) do
+ def delete(%{private: %{open_api_spex: %{params: %{name: document_name}}}} = conn, _) do
with :ok <- InstanceDocument.delete(document_name) do
json(conn, %{})
end
diff --git a/lib/pleroma/web/admin_api/controllers/invite_controller.ex b/lib/pleroma/web/admin_api/controllers/invite_controller.ex
@@ -13,7 +13,7 @@ defmodule Pleroma.Web.AdminAPI.InviteController do
require Logger
- plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
plug(OAuthScopesPlug, %{scopes: ["admin:read:invites"]} when action == :index)
plug(
@@ -33,14 +33,14 @@ defmodule Pleroma.Web.AdminAPI.InviteController do
end
@doc "Create an account registration invite token"
- def create(%{body_params: params} = conn, _) do
+ def create(%{private: %{open_api_spex: %{body_params: params}}} = conn, _) do
{:ok, invite} = UserInviteToken.create_invite(params)
render(conn, "show.json", invite: invite)
end
@doc "Revokes invite by token"
- def revoke(%{body_params: %{token: token}} = conn, _) do
+ def revoke(%{private: %{open_api_spex: %{body_params: %{token: token}}}} = conn, _) do
with {:ok, invite} <- UserInviteToken.find_by_token(token),
{:ok, updated_invite} = UserInviteToken.update_invite(invite, %{used: true}) do
render(conn, "show.json", invite: updated_invite)
@@ -51,7 +51,13 @@ defmodule Pleroma.Web.AdminAPI.InviteController do
end
@doc "Sends registration invite via email"
- def email(%{assigns: %{user: user}, body_params: %{email: email} = params} = conn, _) do
+ def email(
+ %{
+ assigns: %{user: user},
+ private: %{open_api_spex: %{body_params: %{email: email} = params}}
+ } = conn,
+ _
+ ) do
with {_, false} <- {:registrations_open, Config.get([:instance, :registrations_open])},
{_, true} <- {:invites_enabled, Config.get([:instance, :invites_enabled])},
{:ok, invite_token} <- UserInviteToken.create_invite(),
diff --git a/lib/pleroma/web/admin_api/controllers/media_proxy_cache_controller.ex b/lib/pleroma/web/admin_api/controllers/media_proxy_cache_controller.ex
@@ -11,7 +11,7 @@ defmodule Pleroma.Web.AdminAPI.MediaProxyCacheController do
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
- plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
plug(
OAuthScopesPlug,
@@ -27,7 +27,7 @@ defmodule Pleroma.Web.AdminAPI.MediaProxyCacheController do
defdelegate open_api_operation(action), to: Spec.MediaProxyCacheOperation
- def index(%{assigns: %{user: _}} = conn, params) do
+ def index(%{assigns: %{user: _}, private: %{open_api_spex: %{params: params}}} = conn, _) do
entries = fetch_entries(params)
urls = paginate_entries(entries, params.page, params.page_size)
@@ -59,12 +59,19 @@ defmodule Pleroma.Web.AdminAPI.MediaProxyCacheController do
Enum.slice(entries, offset, page_size)
end
- def delete(%{assigns: %{user: _}, body_params: %{urls: urls}} = conn, _) do
+ def delete(
+ %{assigns: %{user: _}, private: %{open_api_spex: %{body_params: %{urls: urls}}}} = conn,
+ _
+ ) do
MediaProxy.remove_from_banned_urls(urls)
json(conn, %{})
end
- def purge(%{assigns: %{user: _}, body_params: %{urls: urls, ban: ban}} = conn, _) do
+ def purge(
+ %{assigns: %{user: _}, private: %{open_api_spex: %{body_params: %{urls: urls, ban: ban}}}} =
+ conn,
+ _
+ ) do
MediaProxy.Invalidation.purge(urls)
if ban do
diff --git a/lib/pleroma/web/admin_api/controllers/relay_controller.ex b/lib/pleroma/web/admin_api/controllers/relay_controller.ex
@@ -11,7 +11,7 @@ defmodule Pleroma.Web.AdminAPI.RelayController do
require Logger
- plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
plug(
OAuthScopesPlug,
@@ -31,7 +31,13 @@ defmodule Pleroma.Web.AdminAPI.RelayController do
end
end
- def follow(%{assigns: %{user: admin}, body_params: %{relay_url: target}} = conn, _) do
+ def follow(
+ %{
+ assigns: %{user: admin},
+ private: %{open_api_spex: %{body_params: %{relay_url: target}}}
+ } = conn,
+ _
+ ) do
with {:ok, _message} <- Relay.follow(target) do
ModerationLog.insert_log(%{action: "relay_follow", actor: admin, target: target})
@@ -44,7 +50,13 @@ defmodule Pleroma.Web.AdminAPI.RelayController do
end
end
- def unfollow(%{assigns: %{user: admin}, body_params: %{relay_url: target} = params} = conn, _) do
+ def unfollow(
+ %{
+ assigns: %{user: admin},
+ private: %{open_api_spex: %{body_params: %{relay_url: target} = params}}
+ } = conn,
+ _
+ ) do
with {:ok, _message} <- Relay.unfollow(target, %{force: params[:force]}) do
ModerationLog.insert_log(%{action: "relay_unfollow", actor: admin, target: target})
diff --git a/lib/pleroma/web/admin_api/controllers/report_controller.ex b/lib/pleroma/web/admin_api/controllers/report_controller.ex
@@ -18,7 +18,7 @@ defmodule Pleroma.Web.AdminAPI.ReportController do
require Logger
- plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
plug(OAuthScopesPlug, %{scopes: ["admin:read:reports"]} when action in [:index, :show])
plug(
@@ -31,13 +31,13 @@ defmodule Pleroma.Web.AdminAPI.ReportController do
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.ReportOperation
- def index(conn, params) do
+ def index(%{private: %{open_api_spex: %{params: params}}} = conn, _) do
reports = Utils.get_reports(params, params.page, params.page_size)
render(conn, "index.json", reports: reports)
end
- def show(conn, %{id: id}) do
+ def show(%{private: %{open_api_spex: %{params: %{id: id}}}} = conn, _) do
with %Activity{} = report <- Activity.get_report(id) do
render(conn, "show.json", Report.extract_report_info(report))
else
@@ -45,7 +45,13 @@ defmodule Pleroma.Web.AdminAPI.ReportController do
end
end
- def update(%{assigns: %{user: admin}, body_params: %{reports: reports}} = conn, _) do
+ def update(
+ %{
+ assigns: %{user: admin},
+ private: %{open_api_spex: %{body_params: %{reports: reports}}}
+ } = conn,
+ _
+ ) do
result =
Enum.map(reports, fn report ->
case CommonAPI.update_report_state(report.id, report.state) do
@@ -73,9 +79,13 @@ defmodule Pleroma.Web.AdminAPI.ReportController do
end
end
- def notes_create(%{assigns: %{user: user}, body_params: %{content: content}} = conn, %{
- id: report_id
- }) do
+ def notes_create(
+ %{
+ assigns: %{user: user},
+ private: %{open_api_spex: %{body_params: %{content: content}, params: %{id: report_id}}}
+ } = conn,
+ _
+ ) do
with {:ok, _} <- ReportNote.create(user.id, report_id, content),
report <- Activity.get_by_id_with_user_actor(report_id) do
ModerationLog.insert_log(%{
@@ -92,10 +102,20 @@ defmodule Pleroma.Web.AdminAPI.ReportController do
end
end
- def notes_delete(%{assigns: %{user: user}} = conn, %{
- id: note_id,
- report_id: report_id
- }) do
+ def notes_delete(
+ %{
+ assigns: %{user: user},
+ private: %{
+ open_api_spex: %{
+ params: %{
+ id: note_id,
+ report_id: report_id
+ }
+ }
+ }
+ } = conn,
+ _
+ ) do
with {:ok, note} <- ReportNote.destroy(note_id),
report <- Activity.get_by_id_with_user_actor(report_id) do
ModerationLog.insert_log(%{
diff --git a/lib/pleroma/web/admin_api/controllers/rule_controller.ex b/lib/pleroma/web/admin_api/controllers/rule_controller.ex
@@ -0,0 +1,62 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.AdminAPI.RuleController do
+ use Pleroma.Web, :controller
+
+ alias Pleroma.Repo
+ alias Pleroma.Rule
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
+
+ import Pleroma.Web.ControllerHelper,
+ only: [
+ json_response: 3
+ ]
+
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["admin:write"]}
+ when action in [:create, :update, :delete]
+ )
+
+ plug(OAuthScopesPlug, %{scopes: ["admin:read"]} when action == :index)
+
+ action_fallback(AdminAPI.FallbackController)
+
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.RuleOperation
+
+ def index(conn, _) do
+ rules =
+ Rule.query()
+ |> Repo.all()
+
+ render(conn, "index.json", rules: rules)
+ end
+
+ def create(%{body_params: params} = conn, _) do
+ rule =
+ params
+ |> Rule.create()
+
+ render(conn, "show.json", rule: rule)
+ end
+
+ def update(%{body_params: params} = conn, %{id: id}) do
+ rule =
+ params
+ |> Rule.update(id)
+
+ render(conn, "show.json", rule: rule)
+ end
+
+ def delete(conn, %{id: id}) do
+ with {:ok, _} <- Rule.delete(id) do
+ json(conn, %{})
+ else
+ _ -> json_response(conn, :bad_request, "")
+ end
+ end
+end
diff --git a/lib/pleroma/web/admin_api/controllers/user_controller.ex b/lib/pleroma/web/admin_api/controllers/user_controller.ex
@@ -18,7 +18,7 @@ defmodule Pleroma.Web.AdminAPI.UserController do
@users_page_size 50
- plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
plug(
OAuthScopesPlug,
@@ -51,13 +51,22 @@ defmodule Pleroma.Web.AdminAPI.UserController do
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.Admin.UserOperation
- def delete(conn, %{nickname: nickname}) do
+ def delete(%{private: %{open_api_spex: %{params: %{nickname: nickname}}}} = conn, _) do
conn
- |> Map.put(:body_params, %{nicknames: [nickname]})
- |> delete(%{})
+ |> do_deletes([nickname])
end
- def delete(%{assigns: %{user: admin}, body_params: %{nicknames: nicknames}} = conn, _) do
+ def delete(
+ %{
+ private: %{open_api_spex: %{body_params: %{nicknames: nicknames}}}
+ } = conn,
+ _
+ ) do
+ conn
+ |> do_deletes(nicknames)
+ end
+
+ defp do_deletes(%{assigns: %{user: admin}} = conn, nicknames) when is_list(nicknames) do
users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
Enum.each(users, fn user ->
@@ -77,9 +86,13 @@ defmodule Pleroma.Web.AdminAPI.UserController do
def follow(
%{
assigns: %{user: admin},
- body_params: %{
- follower: follower_nick,
- followed: followed_nick
+ private: %{
+ open_api_spex: %{
+ body_params: %{
+ follower: follower_nick,
+ followed: followed_nick
+ }
+ }
}
} = conn,
_
@@ -102,9 +115,13 @@ defmodule Pleroma.Web.AdminAPI.UserController do
def unfollow(
%{
assigns: %{user: admin},
- body_params: %{
- follower: follower_nick,
- followed: followed_nick
+ private: %{
+ open_api_spex: %{
+ body_params: %{
+ follower: follower_nick,
+ followed: followed_nick
+ }
+ }
}
} = conn,
_
@@ -124,7 +141,13 @@ defmodule Pleroma.Web.AdminAPI.UserController do
json(conn, "ok")
end
- def create(%{assigns: %{user: admin}, body_params: %{users: users}} = conn, _) do
+ def create(
+ %{
+ assigns: %{user: admin},
+ private: %{open_api_spex: %{body_params: %{users: users}}}
+ } = conn,
+ _
+ ) do
changesets =
users
|> Enum.map(fn %{nickname: nickname, email: email, password: password} ->
@@ -178,7 +201,13 @@ defmodule Pleroma.Web.AdminAPI.UserController do
end
end
- def show(%{assigns: %{user: admin}} = conn, %{nickname: nickname}) do
+ def show(
+ %{
+ assigns: %{user: admin},
+ private: %{open_api_spex: %{params: %{nickname: nickname}}}
+ } = conn,
+ _
+ ) do
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname, for: admin) do
render(conn, "show.json", %{user: user})
else
@@ -186,7 +215,11 @@ defmodule Pleroma.Web.AdminAPI.UserController do
end
end
- def toggle_activation(%{assigns: %{user: admin}} = conn, %{nickname: nickname}) do
+ def toggle_activation(
+ %{assigns: %{user: admin}, private: %{open_api_spex: %{params: %{nickname: nickname}}}} =
+ conn,
+ _
+ ) do
user = User.get_cached_by_nickname(nickname)
{:ok, updated_user} = User.set_activation(user, !user.is_active)
@@ -202,7 +235,13 @@ defmodule Pleroma.Web.AdminAPI.UserController do
render(conn, "show.json", user: updated_user)
end
- def activate(%{assigns: %{user: admin}, body_params: %{nicknames: nicknames}} = conn, _) do
+ def activate(
+ %{
+ assigns: %{user: admin},
+ private: %{open_api_spex: %{body_params: %{nicknames: nicknames}}}
+ } = conn,
+ _
+ ) do
users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
{:ok, updated_users} = User.set_activation(users, true)
@@ -212,10 +251,16 @@ defmodule Pleroma.Web.AdminAPI.UserController do
action: "activate"
})
- render(conn, "index.json", users: Keyword.values(updated_users))
+ render(conn, "index.json", users: updated_users)
end
- def deactivate(%{assigns: %{user: admin}, body_params: %{nicknames: nicknames}} = conn, _) do
+ def deactivate(
+ %{
+ assigns: %{user: admin},
+ private: %{open_api_spex: %{body_params: %{nicknames: nicknames}}}
+ } = conn,
+ _
+ ) do
users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
{:ok, updated_users} = User.set_activation(users, false)
@@ -225,10 +270,16 @@ defmodule Pleroma.Web.AdminAPI.UserController do
action: "deactivate"
})
- render(conn, "index.json", users: Keyword.values(updated_users))
+ render(conn, "index.json", users: updated_users)
end
- def approve(%{assigns: %{user: admin}, body_params: %{nicknames: nicknames}} = conn, _) do
+ def approve(
+ %{
+ assigns: %{user: admin},
+ private: %{open_api_spex: %{body_params: %{nicknames: nicknames}}}
+ } = conn,
+ _
+ ) do
users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
{:ok, updated_users} = User.approve(users)
@@ -241,7 +292,13 @@ defmodule Pleroma.Web.AdminAPI.UserController do
render(conn, "index.json", users: updated_users)
end
- def suggest(%{assigns: %{user: admin}, body_params: %{nicknames: nicknames}} = conn, _) do
+ def suggest(
+ %{
+ assigns: %{user: admin},
+ private: %{open_api_spex: %{body_params: %{nicknames: nicknames}}}
+ } = conn,
+ _
+ ) do
users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
{:ok, updated_users} = User.set_suggestion(users, true)
@@ -254,7 +311,13 @@ defmodule Pleroma.Web.AdminAPI.UserController do
render(conn, "index.json", users: updated_users)
end
- def unsuggest(%{assigns: %{user: admin}, body_params: %{nicknames: nicknames}} = conn, _) do
+ def unsuggest(
+ %{
+ assigns: %{user: admin},
+ private: %{open_api_spex: %{body_params: %{nicknames: nicknames}}}
+ } = conn,
+ _
+ ) do
users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
{:ok, updated_users} = User.set_suggestion(users, false)
@@ -267,7 +330,7 @@ defmodule Pleroma.Web.AdminAPI.UserController do
render(conn, "index.json", users: updated_users)
end
- def index(conn, params) do
+ def index(%{private: %{open_api_spex: %{params: params}}} = conn, _) do
{page, page_size} = page_params(params)
filters = maybe_parse_filters(params[:filters])
diff --git a/lib/pleroma/web/admin_api/views/report_view.ex b/lib/pleroma/web/admin_api/views/report_view.ex
@@ -6,9 +6,11 @@ defmodule Pleroma.Web.AdminAPI.ReportView do
use Pleroma.Web, :view
alias Pleroma.HTML
+ alias Pleroma.Rule
alias Pleroma.User
alias Pleroma.Web.AdminAPI
alias Pleroma.Web.AdminAPI.Report
+ alias Pleroma.Web.AdminAPI.RuleView
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MastodonAPI.StatusView
@@ -46,7 +48,8 @@ defmodule Pleroma.Web.AdminAPI.ReportView do
as: :activity
}),
state: report.data["state"],
- notes: render(__MODULE__, "index_notes.json", %{notes: report.report_notes})
+ notes: render(__MODULE__, "index_notes.json", %{notes: report.report_notes}),
+ rules: rules(Map.get(report.data, "rules", nil))
}
end
@@ -71,4 +74,16 @@ defmodule Pleroma.Web.AdminAPI.ReportView do
created_at: Utils.to_masto_date(inserted_at)
}
end
+
+ defp rules(nil) do
+ []
+ end
+
+ defp rules(rule_ids) do
+ rules =
+ rule_ids
+ |> Rule.get()
+
+ render(RuleView, "index.json", rules: rules)
+ end
end
diff --git a/lib/pleroma/web/admin_api/views/rule_view.ex b/lib/pleroma/web/admin_api/views/rule_view.ex
@@ -0,0 +1,22 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.AdminAPI.RuleView do
+ use Pleroma.Web, :view
+
+ require Pleroma.Constants
+
+ def render("index.json", %{rules: rules} = _opts) do
+ render_many(rules, __MODULE__, "show.json")
+ end
+
+ def render("show.json", %{rule: rule} = _opts) do
+ %{
+ id: to_string(rule.id),
+ priority: rule.priority,
+ text: rule.text,
+ hint: rule.hint
+ }
+ end
+end
diff --git a/lib/pleroma/web/api_spec.ex b/lib/pleroma/web/api_spec.ex
@@ -43,7 +43,7 @@ defmodule Pleroma.Web.ApiSpec do
- [Mastodon API documentation](https://docs.joinmastodon.org/client/intro/)
- [Differences in Mastodon API responses from vanilla Mastodon](https://docs-develop.pleroma.social/backend/development/API/differences_in_mastoapi_responses/)
- Please report such occurences on our [issue tracker](https://git.pleroma.social/pleroma/pleroma/-/issues). Feel free to submit API questions or proposals there too!
+ Please report such occurrences on our [issue tracker](https://git.pleroma.social/pleroma/pleroma/-/issues). Feel free to submit API questions or proposals there too!
""",
# Strip environment from the version
version: Application.spec(:pleroma, :vsn) |> to_string() |> String.replace(~r/\+.*$/, ""),
@@ -94,14 +94,15 @@ defmodule Pleroma.Web.ApiSpec do
"tags" => [
"Chat administration",
"Emoji pack administration",
- "Frontend managment",
+ "Frontend management",
"Instance configuration",
"Instance documents",
+ "Instance rule managment",
"Invites",
"MediaProxy cache",
- "OAuth application managment",
+ "OAuth application management",
"Relays",
- "Report managment",
+ "Report management",
"Status administration",
"User administration",
"Announcement management"
@@ -137,7 +138,8 @@ defmodule Pleroma.Web.ApiSpec do
"Scheduled statuses",
"Search",
"Status actions",
- "Media attachments"
+ "Media attachments",
+ "Bookmark folders"
]
},
%{
diff --git a/lib/pleroma/web/api_spec/cast_and_validate.ex b/lib/pleroma/web/api_spec/cast_and_validate.ex
@@ -27,10 +27,12 @@ defmodule Pleroma.Web.ApiSpec.CastAndValidate do
@impl Plug
- def call(conn, %{operation_id: operation_id, render_error: render_error}) do
+ def call(conn, %{operation_id: operation_id, render_error: render_error} = opts) do
{spec, operation_lookup} = PutApiSpec.get_spec_and_operation_lookup(conn)
operation = operation_lookup[operation_id]
+ cast_opts = opts |> Map.take([:replace_params]) |> Map.to_list()
+
content_type =
case Conn.get_req_header(conn, "content-type") do
[header_value | _] ->
@@ -44,7 +46,7 @@ defmodule Pleroma.Web.ApiSpec.CastAndValidate do
conn = Conn.put_private(conn, :operation_id, operation_id)
- case cast_and_validate(spec, operation, conn, content_type, strict?()) do
+ case cast_and_validate(spec, operation, conn, content_type, strict?(), cast_opts) do
{:ok, conn} ->
conn
@@ -94,11 +96,11 @@ defmodule Pleroma.Web.ApiSpec.CastAndValidate do
def call(conn, opts), do: OpenApiSpex.Plug.CastAndValidate.call(conn, opts)
- defp cast_and_validate(spec, operation, conn, content_type, true = _strict) do
- OpenApiSpex.cast_and_validate(spec, operation, conn, content_type)
+ defp cast_and_validate(spec, operation, conn, content_type, true = _strict, cast_opts) do
+ OpenApiSpex.cast_and_validate(spec, operation, conn, content_type, cast_opts)
end
- defp cast_and_validate(spec, operation, conn, content_type, false = _strict) do
+ defp cast_and_validate(spec, operation, conn, content_type, false = _strict, cast_opts) do
case OpenApiSpex.cast_and_validate(spec, operation, conn, content_type) do
{:ok, conn} ->
{:ok, conn}
@@ -123,7 +125,7 @@ defmodule Pleroma.Web.ApiSpec.CastAndValidate do
end)
conn = %Conn{conn | query_params: query_params}
- OpenApiSpex.cast_and_validate(spec, operation, conn, content_type)
+ OpenApiSpex.cast_and_validate(spec, operation, conn, content_type, cast_opts)
end
end
diff --git a/lib/pleroma/web/api_spec/helpers.ex b/lib/pleroma/web/api_spec/helpers.ex
@@ -62,7 +62,7 @@ defmodule Pleroma.Web.ApiSpec.Helpers do
Operation.parameter(
:with_relationships,
:query,
- BooleanLike,
+ BooleanLike.schema(),
"Embed relationships into accounts. **If this parameter is not set account's `pleroma.relationship` is going to be `null`.**"
)
end
diff --git a/lib/pleroma/web/api_spec/operations/account_operation.ex b/lib/pleroma/web/api_spec/operations/account_operation.ex
@@ -11,6 +11,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
alias Pleroma.Web.ApiSpec.Schemas.ActorType
alias Pleroma.Web.ApiSpec.Schemas.ApiError
alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
+ alias Pleroma.Web.ApiSpec.Schemas.FlakeID
alias Pleroma.Web.ApiSpec.Schemas.List
alias Pleroma.Web.ApiSpec.Schemas.Status
alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope
@@ -122,22 +123,27 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
parameters:
[
%Reference{"$ref": "#/components/parameters/accountIdOrNickname"},
- Operation.parameter(:pinned, :query, BooleanLike, "Include only pinned statuses"),
+ Operation.parameter(
+ :pinned,
+ :query,
+ BooleanLike.schema(),
+ "Include only pinned statuses"
+ ),
Operation.parameter(:tagged, :query, :string, "With tag"),
Operation.parameter(
:only_media,
:query,
- BooleanLike,
+ BooleanLike.schema(),
"Include only statuses with media attached"
),
Operation.parameter(
:with_muted,
:query,
- BooleanLike,
+ BooleanLike.schema(),
"Include statuses from muted accounts."
),
- Operation.parameter(:exclude_reblogs, :query, BooleanLike, "Exclude reblogs"),
- Operation.parameter(:exclude_replies, :query, BooleanLike, "Exclude replies"),
+ Operation.parameter(:exclude_reblogs, :query, BooleanLike.schema(), "Exclude reblogs"),
+ Operation.parameter(:exclude_replies, :query, BooleanLike.schema(), "Exclude replies"),
Operation.parameter(
:exclude_visibilities,
:query,
@@ -147,7 +153,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
Operation.parameter(
:with_muted,
:query,
- BooleanLike,
+ BooleanLike.schema(),
"Include reactions from muted accounts."
)
] ++ pagination_params(),
@@ -347,7 +353,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
summary: "Endorse",
operationId: "AccountController.endorse",
security: [%{"oAuth" => ["follow", "write:accounts"]}],
- description: "Addds the given account to endorsed accounts list.",
+ description: "Adds the given account to endorsed accounts list.",
parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}],
responses: %{
200 => Operation.response("Relationship", "application/json", AccountRelationship),
@@ -508,6 +514,48 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
}
end
+ def familiar_followers_operation do
+ %Operation{
+ tags: ["Retrieve account information"],
+ summary: "Followers that you follow",
+ operationId: "AccountController.familiar_followers",
+ description:
+ "Obtain a list of all accounts that follow a given account, filtered for accounts you follow.",
+ security: [%{"oAuth" => ["read:follows"]}],
+ parameters: [
+ Operation.parameter(
+ :id,
+ :query,
+ %Schema{
+ oneOf: [%Schema{type: :array, items: %Schema{type: :string}}, %Schema{type: :string}]
+ },
+ "Account IDs",
+ example: "123"
+ )
+ ],
+ responses: %{
+ 200 =>
+ Operation.response("Accounts", "application/json", %Schema{
+ title: "ArrayOfAccounts",
+ type: :array,
+ items: %Schema{
+ title: "Account",
+ type: :object,
+ properties: %{
+ id: FlakeID,
+ accounts: %Schema{
+ title: "ArrayOfAccounts",
+ type: :array,
+ items: Account,
+ example: [Account.schema().example]
+ }
+ }
+ }
+ })
+ }
+ }
+ end
+
defp create_request do
%Schema{
title: "AccountCreateRequest",
diff --git a/lib/pleroma/web/api_spec/operations/admin/frontend_operation.ex b/lib/pleroma/web/api_spec/operations/admin/frontend_operation.ex
@@ -16,7 +16,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.FrontendOperation do
def index_operation do
%Operation{
- tags: ["Frontend managment"],
+ tags: ["Frontend management"],
summary: "Retrieve a list of available frontends",
operationId: "AdminAPI.FrontendController.index",
security: [%{"oAuth" => ["admin:read"]}],
@@ -29,7 +29,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.FrontendOperation do
def install_operation do
%Operation{
- tags: ["Frontend managment"],
+ tags: ["Frontend management"],
summary: "Install a frontend",
operationId: "AdminAPI.FrontendController.install",
security: [%{"oAuth" => ["admin:read"]}],
diff --git a/lib/pleroma/web/api_spec/operations/admin/o_auth_app_operation.ex b/lib/pleroma/web/api_spec/operations/admin/o_auth_app_operation.ex
@@ -17,7 +17,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.OAuthAppOperation do
def index_operation do
%Operation{
summary: "Retrieve a list of OAuth applications",
- tags: ["OAuth application managment"],
+ tags: ["OAuth application management"],
operationId: "AdminAPI.OAuthAppController.index",
security: [%{"oAuth" => ["admin:write"]}],
parameters: [
@@ -69,7 +69,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.OAuthAppOperation do
def create_operation do
%Operation{
- tags: ["OAuth application managment"],
+ tags: ["OAuth application management"],
summary: "Create an OAuth application",
operationId: "AdminAPI.OAuthAppController.create",
requestBody: request_body("Parameters", create_request()),
@@ -84,7 +84,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.OAuthAppOperation do
def update_operation do
%Operation{
- tags: ["OAuth application managment"],
+ tags: ["OAuth application management"],
summary: "Update OAuth application",
operationId: "AdminAPI.OAuthAppController.update",
parameters: [id_param() | admin_api_params()],
@@ -102,7 +102,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.OAuthAppOperation do
def delete_operation do
%Operation{
- tags: ["OAuth application managment"],
+ tags: ["OAuth application management"],
summary: "Delete OAuth application",
operationId: "AdminAPI.OAuthAppController.delete",
parameters: [id_param() | admin_api_params()],
diff --git a/lib/pleroma/web/api_spec/operations/admin/report_operation.ex b/lib/pleroma/web/api_spec/operations/admin/report_operation.ex
@@ -19,7 +19,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do
def index_operation do
%Operation{
- tags: ["Report managment"],
+ tags: ["Report management"],
summary: "Retrieve a list of reports",
operationId: "AdminAPI.ReportController.index",
security: [%{"oAuth" => ["admin:read:reports"]}],
@@ -31,6 +31,12 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do
"Filter by report state"
),
Operation.parameter(
+ :rule_id,
+ :query,
+ %Schema{type: :string},
+ "Filter by selected rule id"
+ ),
+ Operation.parameter(
:limit,
:query,
%Schema{type: :integer},
@@ -69,7 +75,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do
def show_operation do
%Operation{
- tags: ["Report managment"],
+ tags: ["Report management"],
summary: "Retrieve a report",
operationId: "AdminAPI.ReportController.show",
parameters: [id_param() | admin_api_params()],
@@ -83,7 +89,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do
def update_operation do
%Operation{
- tags: ["Report managment"],
+ tags: ["Report management"],
summary: "Change state of specified reports",
operationId: "AdminAPI.ReportController.update",
security: [%{"oAuth" => ["admin:write:reports"]}],
@@ -99,7 +105,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do
def notes_create_operation do
%Operation{
- tags: ["Report managment"],
+ tags: ["Report management"],
summary: "Add a note to the report",
operationId: "AdminAPI.ReportController.notes_create",
parameters: [id_param() | admin_api_params()],
@@ -120,7 +126,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do
def notes_delete_operation do
%Operation{
- tags: ["Report managment"],
+ tags: ["Report management"],
summary: "Delete note attached to the report",
operationId: "AdminAPI.ReportController.notes_delete",
parameters: [
@@ -141,7 +147,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do
end
def id_param do
- Operation.parameter(:id, :path, FlakeID, "Report ID",
+ Operation.parameter(:id, :path, FlakeID.schema(), "Report ID",
example: "9umDrYheeY451cQnEe",
required: true
)
@@ -169,6 +175,17 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do
inserted_at: %Schema{type: :string, format: :"date-time"}
}
}
+ },
+ rules: %Schema{
+ type: :array,
+ items: %Schema{
+ type: :object,
+ properties: %{
+ id: %Schema{type: :string},
+ text: %Schema{type: :string},
+ hint: %Schema{type: :string, nullable: true}
+ }
+ }
}
}
}
diff --git a/lib/pleroma/web/api_spec/operations/admin/rule_operation.ex b/lib/pleroma/web/api_spec/operations/admin/rule_operation.ex
@@ -0,0 +1,115 @@
+# 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.Admin.RuleOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.ApiError
+
+ import Pleroma.Web.ApiSpec.Helpers
+
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ def index_operation do
+ %Operation{
+ tags: ["Instance rule managment"],
+ summary: "Retrieve list of instance rules",
+ operationId: "AdminAPI.RuleController.index",
+ security: [%{"oAuth" => ["admin:read"]}],
+ responses: %{
+ 200 =>
+ Operation.response("Response", "application/json", %Schema{
+ type: :array,
+ items: rule()
+ }),
+ 403 => Operation.response("Forbidden", "application/json", ApiError)
+ }
+ }
+ end
+
+ def create_operation do
+ %Operation{
+ tags: ["Instance rule managment"],
+ summary: "Create new rule",
+ operationId: "AdminAPI.RuleController.create",
+ security: [%{"oAuth" => ["admin:write"]}],
+ parameters: admin_api_params(),
+ requestBody: request_body("Parameters", create_request(), required: true),
+ responses: %{
+ 200 => Operation.response("Response", "application/json", rule()),
+ 400 => Operation.response("Bad Request", "application/json", ApiError),
+ 403 => Operation.response("Forbidden", "application/json", ApiError)
+ }
+ }
+ end
+
+ def update_operation do
+ %Operation{
+ tags: ["Instance rule managment"],
+ summary: "Modify existing rule",
+ operationId: "AdminAPI.RuleController.update",
+ security: [%{"oAuth" => ["admin:write"]}],
+ parameters: [Operation.parameter(:id, :path, :string, "Rule ID")],
+ requestBody: request_body("Parameters", update_request(), required: true),
+ responses: %{
+ 200 => Operation.response("Response", "application/json", rule()),
+ 400 => Operation.response("Bad Request", "application/json", ApiError),
+ 403 => Operation.response("Forbidden", "application/json", ApiError)
+ }
+ }
+ end
+
+ def delete_operation do
+ %Operation{
+ tags: ["Instance rule managment"],
+ summary: "Delete rule",
+ operationId: "AdminAPI.RuleController.delete",
+ parameters: [Operation.parameter(:id, :path, :string, "Rule ID")],
+ security: [%{"oAuth" => ["admin:write"]}],
+ responses: %{
+ 200 => empty_object_response(),
+ 404 => Operation.response("Not Found", "application/json", ApiError),
+ 403 => Operation.response("Forbidden", "application/json", ApiError)
+ }
+ }
+ end
+
+ defp create_request do
+ %Schema{
+ type: :object,
+ required: [:text],
+ properties: %{
+ priority: %Schema{type: :integer},
+ text: %Schema{type: :string},
+ hint: %Schema{type: :string}
+ }
+ }
+ end
+
+ defp update_request do
+ %Schema{
+ type: :object,
+ properties: %{
+ priority: %Schema{type: :integer},
+ text: %Schema{type: :string},
+ hint: %Schema{type: :string}
+ }
+ }
+ end
+
+ defp rule do
+ %Schema{
+ type: :object,
+ properties: %{
+ id: %Schema{type: :string},
+ priority: %Schema{type: :integer},
+ text: %Schema{type: :string},
+ hint: %Schema{type: :string, nullable: true}
+ }
+ }
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/chat_operation.ex b/lib/pleroma/web/api_spec/operations/chat_operation.ex
@@ -137,7 +137,12 @@ defmodule Pleroma.Web.ApiSpec.ChatOperation do
"Deprecated due to no support for pagination. Using [/api/v2/pleroma/chats](#operation/ChatController.index2) instead is recommended.",
operationId: "ChatController.index",
parameters: [
- Operation.parameter(:with_muted, :query, BooleanLike, "Include chats from muted users")
+ Operation.parameter(
+ :with_muted,
+ :query,
+ BooleanLike.schema(),
+ "Include chats from muted users"
+ )
],
responses: %{
200 => Operation.response("The chats of the user", "application/json", chats_response())
@@ -156,7 +161,12 @@ defmodule Pleroma.Web.ApiSpec.ChatOperation do
summary: "Retrieve list of chats",
operationId: "ChatController.index2",
parameters: [
- Operation.parameter(:with_muted, :query, BooleanLike, "Include chats from muted users")
+ Operation.parameter(
+ :with_muted,
+ :query,
+ BooleanLike.schema(),
+ "Include chats from muted users"
+ )
| pagination_params()
],
responses: %{
diff --git a/lib/pleroma/web/api_spec/operations/directory_operation.ex b/lib/pleroma/web/api_spec/operations/directory_operation.ex
@@ -29,7 +29,7 @@ defmodule Pleroma.Web.ApiSpec.DirectoryOperation do
"Order by recent activity or account creation",
required: nil
),
- Operation.parameter(:local, :query, BooleanLike, "Include local users only")
+ Operation.parameter(:local, :query, BooleanLike.schema(), "Include local users only")
] ++ pagination_params(),
responses: %{
200 =>
diff --git a/lib/pleroma/web/api_spec/operations/emoji_reaction_operation.ex b/lib/pleroma/web/api_spec/operations/emoji_reaction_operation.ex
@@ -21,7 +21,7 @@ defmodule Pleroma.Web.ApiSpec.EmojiReactionOperation do
summary:
"Get an object of emoji to account mappings with accounts that reacted to the post",
parameters: [
- Operation.parameter(:id, :path, FlakeID, "Status ID", required: true),
+ Operation.parameter(:id, :path, FlakeID.schema(), "Status ID", required: true),
Operation.parameter(:emoji, :path, :string, "Filter by a single unicode emoji",
required: nil
),
@@ -45,7 +45,7 @@ defmodule Pleroma.Web.ApiSpec.EmojiReactionOperation do
tags: ["Emoji reactions"],
summary: "React to a post with a unicode emoji",
parameters: [
- Operation.parameter(:id, :path, FlakeID, "Status ID", required: true),
+ Operation.parameter(:id, :path, FlakeID.schema(), "Status ID", required: true),
Operation.parameter(:emoji, :path, :string, "A single character unicode emoji",
required: true
)
@@ -64,7 +64,7 @@ defmodule Pleroma.Web.ApiSpec.EmojiReactionOperation do
tags: ["Emoji reactions"],
summary: "Remove a reaction to a post with a unicode emoji",
parameters: [
- Operation.parameter(:id, :path, FlakeID, "Status ID", required: true),
+ Operation.parameter(:id, :path, FlakeID.schema(), "Status ID", required: true),
Operation.parameter(:emoji, :path, :string, "A single character unicode emoji",
required: true
)
diff --git a/lib/pleroma/web/api_spec/operations/instance_operation.ex b/lib/pleroma/web/api_spec/operations/instance_operation.ex
@@ -23,6 +23,18 @@ defmodule Pleroma.Web.ApiSpec.InstanceOperation do
}
end
+ def show2_operation do
+ %Operation{
+ tags: ["Instance misc"],
+ summary: "Retrieve instance information",
+ description: "Information about the server",
+ operationId: "InstanceController.show2",
+ responses: %{
+ 200 => Operation.response("Instance", "application/json", instance2())
+ }
+ }
+ end
+
def peers_operation do
%Operation{
tags: ["Instance misc"],
@@ -34,10 +46,30 @@ defmodule Pleroma.Web.ApiSpec.InstanceOperation do
}
end
+ def rules_operation do
+ %Operation{
+ tags: ["Instance misc"],
+ summary: "Retrieve list of instance rules",
+ operationId: "InstanceController.rules",
+ responses: %{
+ 200 => Operation.response("Array of domains", "application/json", array_of_rules())
+ }
+ }
+ end
+
defp instance do
%Schema{
type: :object,
properties: %{
+ accounts: %Schema{
+ type: :object,
+ properties: %{
+ max_featured_tags: %Schema{
+ type: :integer,
+ description: "The maximum number of featured tags allowed for each account."
+ }
+ }
+ },
uri: %Schema{type: :string, description: "The domain name of the instance"},
title: %Schema{type: :string, description: "The title of the website"},
description: %Schema{
@@ -89,7 +121,7 @@ defmodule Pleroma.Web.ApiSpec.InstanceOperation do
languages: %Schema{
type: :array,
items: %Schema{type: :string},
- description: "Primary langauges of the website and its staff"
+ description: "Primary languages of the website and its staff"
},
registrations: %Schema{type: :boolean, description: "Whether registrations are enabled"},
# Extra (not present in Mastodon):
@@ -160,7 +192,186 @@ defmodule Pleroma.Web.ApiSpec.InstanceOperation do
"urls" => %{
"streaming_api" => "wss://lain.com"
},
- "version" => "2.7.2 (compatible; Pleroma 2.0.50-536-g25eec6d7-develop)"
+ "version" => "2.7.2 (compatible; Pleroma 2.0.50-536-g25eec6d7-develop)",
+ "rules" => array_of_rules()
+ }
+ }
+ end
+
+ defp instance2 do
+ %Schema{
+ type: :object,
+ properties: %{
+ domain: %Schema{type: :string, description: "The domain name of the instance"},
+ title: %Schema{type: :string, description: "The title of the website"},
+ version: %Schema{
+ type: :string,
+ description: "The version of Pleroma installed on the instance"
+ },
+ source_url: %Schema{
+ type: :string,
+ description: "The version of Pleroma installed on the instance"
+ },
+ description: %Schema{
+ type: :string,
+ description: "Admin-defined description of the Pleroma site"
+ },
+ usage: %Schema{
+ type: :object,
+ description: "Instance usage statistics",
+ properties: %{
+ users: %Schema{
+ type: :object,
+ description: "User count statistics",
+ properties: %{
+ active_month: %Schema{
+ type: :integer,
+ description: "Monthly active users"
+ }
+ }
+ }
+ }
+ },
+ email: %Schema{
+ type: :string,
+ description: "An email that may be contacted for any inquiries",
+ format: :email
+ },
+ urls: %Schema{
+ type: :object,
+ description: "URLs of interest for clients apps",
+ properties: %{}
+ },
+ stats: %Schema{
+ type: :object,
+ description: "Statistics about how much information the instance contains",
+ properties: %{
+ user_count: %Schema{
+ type: :integer,
+ description: "Users registered on this instance"
+ },
+ status_count: %Schema{
+ type: :integer,
+ description: "Statuses authored by users on instance"
+ },
+ domain_count: %Schema{
+ type: :integer,
+ description: "Domains federated with this instance"
+ }
+ }
+ },
+ thumbnail: %Schema{
+ type: :object,
+ properties: %{
+ url: %Schema{
+ type: :string,
+ description: "Banner image for the website",
+ nullable: true
+ }
+ }
+ },
+ languages: %Schema{
+ type: :array,
+ items: %Schema{type: :string},
+ description: "Primary languages of the website and its staff"
+ },
+ registrations: %Schema{
+ type: :object,
+ description: "Registrations-related configuration",
+ properties: %{
+ enabled: %Schema{
+ type: :boolean,
+ description: "Whether registrations are enabled"
+ },
+ approval_required: %Schema{
+ type: :boolean,
+ description: "Whether users need to be manually approved by admin"
+ }
+ }
+ },
+ configuration: %Schema{
+ type: :object,
+ description: "Instance configuration",
+ properties: %{
+ accounts: %Schema{
+ type: :object,
+ properties: %{
+ max_featured_tags: %Schema{
+ type: :integer,
+ description: "The maximum number of featured tags allowed for each account."
+ },
+ max_pinned_statuses: %Schema{
+ type: :integer,
+ description: "The maximum number of pinned statuses for each account."
+ }
+ }
+ },
+ urls: %Schema{
+ type: :object,
+ properties: %{
+ streaming: %Schema{
+ type: :string,
+ description: "Websockets address for push streaming"
+ }
+ }
+ },
+ statuses: %Schema{
+ type: :object,
+ description: "A map with poll limits for local statuses",
+ properties: %{
+ characters_reserved_per_url: %Schema{
+ type: :integer,
+ description:
+ "Each URL in a status will be assumed to be exactly this many characters."
+ },
+ max_characters: %Schema{
+ type: :integer,
+ description: "Posts character limit (CW/Subject included in the counter)"
+ },
+ max_media_attachments: %Schema{
+ type: :integer,
+ description: "Media attachment limit"
+ }
+ }
+ },
+ media_attachments: %Schema{
+ type: :object,
+ description: "A map with poll limits for media attachments",
+ properties: %{
+ image_size_limit: %Schema{
+ type: :integer,
+ description: "File size limit of uploaded images"
+ },
+ video_size_limit: %Schema{
+ type: :integer,
+ description: "File size limit of uploaded videos"
+ }
+ }
+ },
+ polls: %Schema{
+ type: :object,
+ description: "A map with poll limits for local polls",
+ properties: %{
+ max_options: %Schema{
+ type: :integer,
+ description: "Maximum number of options."
+ },
+ max_characters_per_option: %Schema{
+ type: :integer,
+ description: "Maximum number of characters per option."
+ },
+ min_expiration: %Schema{
+ type: :integer,
+ description: "Minimum expiration time (in seconds)."
+ },
+ max_expiration: %Schema{
+ type: :integer,
+ description: "Maximum expiration time (in seconds)."
+ }
+ }
+ }
+ }
+ }
}
}
end
@@ -172,4 +383,18 @@ defmodule Pleroma.Web.ApiSpec.InstanceOperation do
example: ["pleroma.site", "lain.com", "bikeshed.party"]
}
end
+
+ defp array_of_rules do
+ %Schema{
+ type: :array,
+ items: %Schema{
+ type: :object,
+ properties: %{
+ id: %Schema{type: :string},
+ text: %Schema{type: :string},
+ hint: %Schema{type: :string}
+ }
+ }
+ }
+ end
end
diff --git a/lib/pleroma/web/api_spec/operations/notification_operation.ex b/lib/pleroma/web/api_spec/operations/notification_operation.ex
@@ -62,7 +62,7 @@ defmodule Pleroma.Web.ApiSpec.NotificationOperation do
Operation.parameter(
:with_muted,
:query,
- BooleanLike,
+ BooleanLike.schema(),
"Include the notifications from muted users"
)
] ++ pagination_params(),
diff --git a/lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex
@@ -142,7 +142,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaAccountOperation do
end
defp id_param do
- Operation.parameter(:id, :path, FlakeID, "Account ID",
+ Operation.parameter(:id, :path, FlakeID.schema(), "Account ID",
example: "9umDrYheeY451cQnEe",
required: true
)
diff --git a/lib/pleroma/web/api_spec/operations/pleroma_bookmark_folder_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_bookmark_folder_operation.ex
@@ -0,0 +1,125 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.PleromaBookmarkFolderOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.ApiError
+ alias Pleroma.Web.ApiSpec.Schemas.BookmarkFolder
+ alias Pleroma.Web.ApiSpec.Schemas.FlakeID
+
+ import Pleroma.Web.ApiSpec.Helpers
+
+ @spec open_api_operation(any()) :: any()
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ def index_operation do
+ %Operation{
+ tags: ["Bookmark folders"],
+ summary: "All bookmark folders",
+ security: [%{"oAuth" => ["read:bookmarks"]}],
+ operationId: "PleromaAPI.BookmarkFolderController.index",
+ responses: %{
+ 200 =>
+ Operation.response("Array of Bookmark Folders", "application/json", %Schema{
+ type: :array,
+ items: BookmarkFolder
+ })
+ }
+ }
+ end
+
+ def create_operation do
+ %Operation{
+ tags: ["Bookmark folders"],
+ summary: "Create a bookmark folder",
+ security: [%{"oAuth" => ["write:bookmarks"]}],
+ operationId: "PleromaAPI.BookmarkFolderController.create",
+ requestBody: request_body("Parameters", create_request(), required: true),
+ responses: %{
+ 200 => Operation.response("Bookmark Folder", "application/json", BookmarkFolder),
+ 422 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ def update_operation do
+ %Operation{
+ tags: ["Bookmark folders"],
+ summary: "Update a bookmark folder",
+ security: [%{"oAuth" => ["write:bookmarks"]}],
+ operationId: "PleromaAPI.BookmarkFolderController.update",
+ parameters: [id_param()],
+ requestBody: request_body("Parameters", update_request(), required: true),
+ responses: %{
+ 200 => Operation.response("Bookmark Folder", "application/json", BookmarkFolder),
+ 403 => Operation.response("Forbidden", "application/json", ApiError),
+ 404 => Operation.response("Not Found", "application/json", ApiError),
+ 422 => Operation.response("Error", "application/json", ApiError)
+ }
+ }
+ end
+
+ def delete_operation do
+ %Operation{
+ tags: ["Bookmark folders"],
+ summary: "Delete a bookmark folder",
+ security: [%{"oAuth" => ["write:bookmarks"]}],
+ operationId: "PleromaAPI.BookmarkFolderController.delete",
+ parameters: [id_param()],
+ responses: %{
+ 200 => Operation.response("Bookmark Folder", "application/json", BookmarkFolder),
+ 403 => Operation.response("Forbidden", "application/json", ApiError),
+ 404 => Operation.response("Not Found", "application/json", ApiError)
+ }
+ }
+ end
+
+ defp create_request do
+ %Schema{
+ title: "BookmarkFolderCreateRequest",
+ type: :object,
+ properties: %{
+ name: %Schema{
+ type: :string,
+ description: "Folder name"
+ },
+ emoji: %Schema{
+ type: :string,
+ nullable: true,
+ description: "Folder emoji"
+ }
+ }
+ }
+ end
+
+ defp update_request do
+ %Schema{
+ title: "BookmarkFolderUpdateRequest",
+ type: :object,
+ properties: %{
+ name: %Schema{
+ type: :string,
+ nullable: true,
+ description: "Folder name"
+ },
+ emoji: %Schema{
+ type: :string,
+ nullable: true,
+ description: "Folder emoji"
+ }
+ }
+ }
+ end
+
+ def id_param do
+ Operation.parameter(:id, :path, FlakeID.schema(), "Bookmark Folder ID",
+ example: "9umDrYheeY451cQnEe",
+ required: true
+ )
+ end
+end
diff --git a/lib/pleroma/web/api_spec/operations/pleroma_notification_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_notification_operation.ex
@@ -5,7 +5,6 @@
defmodule Pleroma.Web.ApiSpec.PleromaNotificationOperation do
alias OpenApiSpex.Operation
alias OpenApiSpex.Schema
- alias Pleroma.Web.ApiSpec.NotificationOperation
alias Pleroma.Web.ApiSpec.Schemas.ApiError
import Pleroma.Web.ApiSpec.Helpers
@@ -35,12 +34,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaNotificationOperation do
Operation.response(
"A Notification or array of Notifications",
"application/json",
- %Schema{
- anyOf: [
- %Schema{type: :array, items: NotificationOperation.notification()},
- NotificationOperation.notification()
- ]
- }
+ %Schema{type: :string}
),
400 => Operation.response("Bad Request", "application/json", ApiError)
}
diff --git a/lib/pleroma/web/api_spec/operations/pleroma_scrobble_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_scrobble_operation.ex
@@ -23,7 +23,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaScrobbleOperation do
security: [%{"oAuth" => ["write"]}],
operationId: "PleromaAPI.ScrobbleController.create",
deprecated: true,
- requestBody: request_body("Parameters", create_request(), requried: true),
+ requestBody: request_body("Parameters", create_request(), required: true),
responses: %{
200 => Operation.response("Scrobble", "application/json", scrobble())
}
diff --git a/lib/pleroma/web/api_spec/operations/pleroma_status_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_status_operation.ex
@@ -37,7 +37,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaStatusOperation do
end
def id_param do
- Operation.parameter(:id, :path, FlakeID, "Status ID",
+ Operation.parameter(:id, :path, FlakeID.schema(), "Status ID",
example: "9umDrYheeY451cQnEe",
required: true
)
diff --git a/lib/pleroma/web/api_spec/operations/poll_operation.ex b/lib/pleroma/web/api_spec/operations/poll_operation.ex
@@ -47,7 +47,7 @@ defmodule Pleroma.Web.ApiSpec.PollOperation do
end
defp id_param do
- Operation.parameter(:id, :path, FlakeID, "Poll ID",
+ Operation.parameter(:id, :path, FlakeID.schema(), "Poll ID",
example: "123",
required: true
)
diff --git a/lib/pleroma/web/api_spec/operations/report_operation.ex b/lib/pleroma/web/api_spec/operations/report_operation.ex
@@ -53,6 +53,12 @@ defmodule Pleroma.Web.ApiSpec.ReportOperation do
default: false,
description:
"If the account is remote, should the report be forwarded to the remote admin?"
+ },
+ rule_ids: %Schema{
+ type: :array,
+ nullable: true,
+ items: %Schema{type: :string},
+ description: "Array of rules"
}
},
required: [:account_id],
@@ -60,7 +66,8 @@ defmodule Pleroma.Web.ApiSpec.ReportOperation do
"account_id" => "123",
"status_ids" => ["1337"],
"comment" => "bad status!",
- "forward" => "false"
+ "forward" => "false",
+ "rule_ids" => ["3"]
}
}
end
diff --git a/lib/pleroma/web/api_spec/operations/scheduled_activity_operation.ex b/lib/pleroma/web/api_spec/operations/scheduled_activity_operation.ex
@@ -88,7 +88,7 @@ defmodule Pleroma.Web.ApiSpec.ScheduledActivityOperation do
end
defp id_param do
- Operation.parameter(:id, :path, FlakeID, "Poll ID",
+ Operation.parameter(:id, :path, FlakeID.schema(), "Poll ID",
example: "123",
required: true
)
diff --git a/lib/pleroma/web/api_spec/operations/search_operation.ex b/lib/pleroma/web/api_spec/operations/search_operation.ex
@@ -70,7 +70,7 @@ defmodule Pleroma.Web.ApiSpec.SearchOperation do
Operation.parameter(
:account_id,
:query,
- FlakeID,
+ FlakeID.schema(),
"If provided, statuses returned will be authored only by this account"
),
Operation.parameter(
@@ -116,7 +116,7 @@ defmodule Pleroma.Web.ApiSpec.SearchOperation do
Operation.parameter(
:account_id,
:query,
- FlakeID,
+ FlakeID.schema(),
"If provided, statuses returned will be authored only by this account"
),
Operation.parameter(
diff --git a/lib/pleroma/web/api_spec/operations/status_operation.ex b/lib/pleroma/web/api_spec/operations/status_operation.ex
@@ -39,7 +39,7 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
Operation.parameter(
:with_muted,
:query,
- BooleanLike,
+ BooleanLike.schema(),
"Include reactions from muted acccounts."
)
],
@@ -82,7 +82,7 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
Operation.parameter(
:with_muted,
:query,
- BooleanLike,
+ BooleanLike.schema(),
"Include reactions from muted acccounts."
)
],
@@ -256,6 +256,18 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
description: "Privately bookmark a status",
operationId: "StatusController.bookmark",
parameters: [id_param()],
+ requestBody:
+ request_body("Parameters", %Schema{
+ title: "StatusUpdateRequest",
+ type: :object,
+ properties: %{
+ folder_id: %Schema{
+ nullable: true,
+ allOf: [FlakeID],
+ description: "ID of bookmarks folder, if any"
+ }
+ }
+ }),
responses: %{
200 => status_response()
}
@@ -430,7 +442,15 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
summary: "Bookmarked statuses",
description: "Statuses the user has bookmarked",
operationId: "StatusController.bookmarks",
- parameters: pagination_params(),
+ parameters: [
+ Operation.parameter(
+ :folder_id,
+ :query,
+ FlakeID.schema(),
+ "If provided, only display bookmarks from given folder"
+ )
+ | pagination_params()
+ ],
security: [%{"oAuth" => ["read:bookmarks"]}],
responses: %{
200 => Operation.response("Array of Statuses", "application/json", array_of_statuses())
@@ -534,7 +554,7 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
format: :"date-time",
nullable: true,
description:
- "ISO 8601 Datetime at which to schedule a status. Providing this paramter will cause ScheduledStatus to be returned instead of Status. Must be at least 5 minutes in the future."
+ "ISO 8601 Datetime at which to schedule a status. Providing this parameter will cause ScheduledStatus to be returned instead of Status. Must be at least 5 minutes in the future."
},
language: %Schema{
type: :string,
@@ -546,7 +566,7 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
allOf: [BooleanLike],
nullable: true,
description:
- "If set to `true` the post won't be actually posted, but the status entitiy would still be rendered back. This could be useful for previewing rich text/custom emoji, for example"
+ "If set to `true` the post won't be actually posted, but the status entity would still be rendered back. This could be useful for previewing rich text/custom emoji, for example"
},
content_type: %Schema{
type: :string,
@@ -685,7 +705,7 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
end
def id_param do
- Operation.parameter(:id, :path, FlakeID, "Status ID",
+ Operation.parameter(:id, :path, FlakeID.schema(), "Status ID",
example: "9umDrYheeY451cQnEe",
required: true
)
diff --git a/lib/pleroma/web/api_spec/operations/timeline_operation.ex b/lib/pleroma/web/api_spec/operations/timeline_operation.ex
@@ -176,7 +176,12 @@ defmodule Pleroma.Web.ApiSpec.TimelineOperation do
end
defp with_muted_param do
- Operation.parameter(:with_muted, :query, BooleanLike, "Include activities by muted users")
+ Operation.parameter(
+ :with_muted,
+ :query,
+ BooleanLike.schema(),
+ "Include activities by muted users"
+ )
end
defp exclude_visibilities_param do
diff --git a/lib/pleroma/web/api_spec/operations/twitter_util_operation.ex b/lib/pleroma/web/api_spec/operations/twitter_util_operation.ex
@@ -87,7 +87,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
defp change_password_request do
%Schema{
title: "ChangePasswordRequest",
- description: "POST body for changing the account's passowrd",
+ description: "POST body for changing the account's password",
type: :object,
required: [:password, :new_password, :new_password_confirmation],
properties: %{
@@ -136,23 +136,23 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
}
end
- def update_notificaton_settings_operation do
+ def update_notification_settings_operation do
%Operation{
tags: ["Settings"],
summary: "Update Notification Settings",
security: [%{"oAuth" => ["write:accounts"]}],
- operationId: "UtilController.update_notificaton_settings",
+ operationId: "UtilController.update_notification_settings",
parameters: [
Operation.parameter(
:block_from_strangers,
:query,
- BooleanLike,
+ BooleanLike.schema(),
"blocks notifications from accounts you do not follow"
),
Operation.parameter(
:hide_notification_contents,
:query,
- BooleanLike,
+ BooleanLike.schema(),
"removes the contents of a message from the push notification"
)
],
diff --git a/lib/pleroma/web/api_spec/schemas/attachment.ex b/lib/pleroma/web/api_spec/schemas/attachment.ex
@@ -11,7 +11,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Attachment do
title: "Attachment",
description: "Represents a file or media attachment that can be added to a status.",
type: :object,
- requried: [:id, :url, :preview_url],
+ required: [:id, :url, :preview_url],
properties: %{
id: %Schema{type: :string, description: "The ID of the attachment in the database."},
url: %Schema{
diff --git a/lib/pleroma/web/api_spec/schemas/bookmark_folder.ex b/lib/pleroma/web/api_spec/schemas/bookmark_folder.ex
@@ -0,0 +1,26 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.Schemas.BookmarkFolder do
+ alias OpenApiSpex.Schema
+ alias Pleroma.Web.ApiSpec.Schemas.FlakeID
+
+ require OpenApiSpex
+
+ OpenApiSpex.schema(%{
+ title: "BookmarkFolder",
+ description: "Response schema for a bookmark folder",
+ type: :object,
+ properties: %{
+ id: FlakeID,
+ name: %Schema{type: :string, description: "Folder name"},
+ emoji: %Schema{type: :string, description: "Folder emoji", nullable: true}
+ },
+ example: %{
+ "id" => "9toJCu5YZW7O7gfvH6",
+ "name" => "Read later",
+ "emoji" => nil
+ }
+ })
+end
diff --git a/lib/pleroma/web/api_spec/schemas/poll.ex b/lib/pleroma/web/api_spec/schemas/poll.ex
@@ -56,6 +56,15 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Poll do
}
},
description: "Possible answers for the poll."
+ },
+ pleroma: %Schema{
+ type: :object,
+ properties: %{
+ non_anonymous: %Schema{
+ type: :boolean,
+ description: "Can voters be publicly identified?"
+ }
+ }
}
},
example: %{
@@ -79,7 +88,10 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Poll do
votes_count: 4
}
],
- emojis: []
+ emojis: [],
+ pleroma: %{
+ non_anonymous: false
+ }
}
})
end
diff --git a/lib/pleroma/web/api_spec/schemas/status.ex b/lib/pleroma/web/api_spec/schemas/status.ex
@@ -58,6 +58,10 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
format: :uri,
description: "Preview thumbnail"
},
+ image_description: %Schema{
+ type: :string,
+ description: "Alternate text that describes what is in the thumbnail"
+ },
title: %Schema{type: :string, description: "Title of linked resource"},
description: %Schema{type: :string, description: "Description of preview"}
}
diff --git a/lib/pleroma/web/auth/authenticator.ex b/lib/pleroma/web/auth/authenticator.ex
@@ -5,7 +5,7 @@
defmodule Pleroma.Web.Auth.Authenticator do
@callback get_user(Plug.Conn.t()) :: {:ok, user :: struct()} | {:error, any()}
@callback create_from_registration(Plug.Conn.t(), registration :: struct()) ::
- {:ok, User.t()} | {:error, any()}
+ {:ok, Pleroma.User.t()} | {:error, any()}
@callback get_registration(Plug.Conn.t()) :: {:ok, registration :: struct()} | {:error, any()}
@callback handle_error(Plug.Conn.t(), any()) :: any()
@callback auth_template() :: String.t() | nil
diff --git a/lib/pleroma/web/common_api.ex b/lib/pleroma/web/common_api.ex
@@ -8,6 +8,7 @@ defmodule Pleroma.Web.CommonAPI do
alias Pleroma.Formatter
alias Pleroma.ModerationLog
alias Pleroma.Object
+ alias Pleroma.Rule
alias Pleroma.ThreadMute
alias Pleroma.User
alias Pleroma.UserRelationship
@@ -372,7 +373,7 @@ defmodule Pleroma.Web.CommonAPI do
do: visibility in ~w(public unlisted)
def public_announce?(object, _) do
- Visibility.is_public?(object)
+ Visibility.public?(object)
end
def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
@@ -500,12 +501,12 @@ defmodule Pleroma.Web.CommonAPI do
end
defp activity_is_public(activity) do
- with false <- Visibility.is_public?(activity) do
+ with false <- Visibility.public?(activity) do
{:error, :visibility_error}
end
end
- @spec unpin(String.t(), User.t()) :: {:ok, User.t()} | {:error, term()}
+ @spec unpin(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, term()}
def unpin(id, user) do
with %Activity{} = activity <- create_activity_by_id(id),
{:ok, unpin_data, _} <- Builder.unpin(user, activity.object),
@@ -568,14 +569,16 @@ defmodule Pleroma.Web.CommonAPI do
def report(user, data) do
with {:ok, account} <- get_reported_account(data.account_id),
{:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
- {:ok, statuses} <- get_report_statuses(account, data) do
+ {:ok, statuses} <- get_report_statuses(account, data),
+ rules <- get_report_rules(Map.get(data, :rule_ids, nil)) do
ActivityPub.flag(%{
context: Utils.generate_context_id(),
actor: user,
account: account,
statuses: statuses,
content: content_html,
- forward: Map.get(data, :forward, false)
+ forward: Map.get(data, :forward, false),
+ rules: rules
})
end
end
@@ -587,6 +590,15 @@ defmodule Pleroma.Web.CommonAPI do
end
end
+ defp get_report_rules(nil) do
+ nil
+ end
+
+ defp get_report_rules(rule_ids) do
+ rule_ids
+ |> Enum.filter(&Rule.exists?/1)
+ end
+
def update_report_state(activity_ids, state) when is_list(activity_ids) do
case Utils.update_report_state(activity_ids, state) do
:ok -> {:ok, activity_ids}
diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex
@@ -14,6 +14,8 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
import Pleroma.Web.Gettext
import Pleroma.Web.Utils.Guards, only: [not_empty_string: 1]
+ @type t :: %__MODULE__{}
+
defstruct valid?: true,
errors: [],
user: nil,
diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex
@@ -109,7 +109,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
def get_to_and_cc(%{visibility: "direct"} = draft) do
# If the OP is a DM already, add the implicit actor.
- if draft.in_reply_to && Visibility.is_direct?(draft.in_reply_to) do
+ if draft.in_reply_to && Visibility.direct?(draft.in_reply_to) do
{Enum.uniq([draft.in_reply_to.data["actor"] | draft.mentions]), []}
else
{draft.mentions, []}
diff --git a/lib/pleroma/web/controller_helper.ex b/lib/pleroma/web/controller_helper.ex
@@ -20,7 +20,7 @@ defmodule Pleroma.Web.ControllerHelper do
|> json(json)
end
- @spec fetch_integer_param(map(), String.t(), integer() | nil) :: integer() | nil
+ @spec fetch_integer_param(map(), String.t() | atom(), integer() | nil) :: integer() | nil
def fetch_integer_param(params, name, default \\ nil) do
params
|> Map.get(name, default)
@@ -53,10 +53,15 @@ defmodule Pleroma.Web.ControllerHelper do
end
end
+ # TODO: Only fetch the params from open_api_spex when everything is converted
@id_keys Pagination.page_keys() -- ["limit", "order"]
defp build_pagination_fields(conn, min_id, max_id, extra_params) do
params =
- conn.params
+ if Map.has_key?(conn.private, :open_api_spex) do
+ get_in(conn, [Access.key(:private), Access.key(:open_api_spex), Access.key(:params)])
+ else
+ conn.params
+ end
|> Map.drop(Map.keys(conn.path_params) |> Enum.map(&String.to_existing_atom/1))
|> Map.merge(extra_params)
|> Map.drop(@id_keys)
@@ -85,18 +90,15 @@ defmodule Pleroma.Web.ControllerHelper do
end
end
- def assign_account_by_id(conn, _) do
- case Pleroma.User.get_cached_by_id(conn.params.id) do
+ def assign_account_by_id(%{private: %{open_api_spex: %{params: %{id: id}}}} = conn, _) do
+ case Pleroma.User.get_cached_by_id(id) do
%Pleroma.User{} = account -> assign(conn, :account, account)
nil -> Pleroma.Web.MastodonAPI.FallbackController.call(conn, {:error, :not_found}) |> halt()
end
end
def try_render(conn, target, params) when is_binary(target) do
- case render(conn, target, params) do
- nil -> render_error(conn, :not_implemented, "Can't display this activity")
- res -> res
- end
+ render(conn, target, params)
end
def try_render(conn, _, _) do
diff --git a/lib/pleroma/web/embed_controller.ex b/lib/pleroma/web/embed_controller.ex
@@ -11,12 +11,10 @@ defmodule Pleroma.Web.EmbedController do
alias Pleroma.Web.ActivityPub.Visibility
- plug(:put_layout, :embed)
-
def show(conn, %{"id" => id}) do
with %Activity{local: true} = activity <-
Activity.get_by_id_with_object(id),
- true <- Visibility.is_public?(activity.object) do
+ true <- Visibility.public?(activity.object) do
{:ok, author} = User.get_or_fetch(activity.object.data["actor"])
conn
diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex
@@ -9,6 +9,16 @@ defmodule Pleroma.Web.Endpoint do
alias Pleroma.Config
+ socket("/api/v1/streaming", Pleroma.Web.MastodonAPI.WebsocketHandler,
+ longpoll: false,
+ websocket: [
+ path: "/",
+ compress: false,
+ error_handler: {Pleroma.Web.MastodonAPI.WebsocketHandler, :handle_error, []},
+ fullsweep_after: 20
+ ]
+ )
+
socket("/socket", Pleroma.Web.UserSocket,
websocket: [
path: "/websocket",
@@ -18,7 +28,8 @@ defmodule Pleroma.Web.Endpoint do
],
timeout: 60_000,
transport_log: false,
- compress: false
+ compress: false,
+ fullsweep_after: 20
],
longpoll: false
)
@@ -32,7 +43,8 @@ defmodule Pleroma.Web.Endpoint do
plug(Pleroma.Web.Plugs.HTTPSecurityPlug)
plug(Pleroma.Web.Plugs.UploadedMedia)
- @static_cache_control "public, no-cache"
+ @static_cache_control "public, max-age=1209600"
+ @static_cache_disabled "public, no-cache"
# InstanceStatic needs to be before Plug.Static to be able to override shipped-static files
# If you're adding new paths to `only:` you'll need to configure them in InstanceStatic as well
@@ -43,22 +55,32 @@ defmodule Pleroma.Web.Endpoint do
from: :pleroma,
only: ["emoji", "images"],
gzip: true,
- cache_control_for_etags: "public, max-age=1209600",
+ cache_control_for_etags: @static_cache_control,
headers: %{
- "cache-control" => "public, max-age=1209600"
+ "cache-control" => @static_cache_control
}
)
plug(Pleroma.Web.Plugs.InstanceStatic,
at: "/",
gzip: true,
- cache_control_for_etags: @static_cache_control,
+ cache_control_for_etags: @static_cache_disabled,
headers: %{
- "cache-control" => @static_cache_control
+ "cache-control" => @static_cache_disabled
+ }
+ )
+
+ plug(Pleroma.Web.Plugs.FrontendStatic,
+ at: "/",
+ frontend_type: :primary,
+ only: ["index.html"],
+ gzip: true,
+ cache_control_for_etags: @static_cache_disabled,
+ headers: %{
+ "cache-control" => @static_cache_disabled
}
)
- # Careful! No `only` restriction here, as we don't know what frontends contain.
plug(Pleroma.Web.Plugs.FrontendStatic,
at: "/",
frontend_type: :primary,
@@ -75,9 +97,9 @@ defmodule Pleroma.Web.Endpoint do
at: "/pleroma/admin",
frontend_type: :admin,
gzip: true,
- cache_control_for_etags: @static_cache_control,
+ cache_control_for_etags: @static_cache_disabled,
headers: %{
- "cache-control" => @static_cache_control
+ "cache-control" => @static_cache_disabled
}
)
@@ -92,9 +114,9 @@ defmodule Pleroma.Web.Endpoint do
only: Pleroma.Constants.static_only_files(),
# credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength
gzip: true,
- cache_control_for_etags: @static_cache_control,
+ cache_control_for_etags: @static_cache_disabled,
headers: %{
- "cache-control" => @static_cache_control
+ "cache-control" => @static_cache_disabled
}
)
diff --git a/lib/pleroma/web/federator.ex b/lib/pleroma/web/federator.ex
@@ -6,9 +6,9 @@ defmodule Pleroma.Web.Federator do
alias Pleroma.Activity
alias Pleroma.Object.Containment
alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.Publisher
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.Utils
- alias Pleroma.Web.Federator.Publisher
alias Pleroma.Workers.PublisherWorker
alias Pleroma.Workers.ReceiverWorker
@@ -68,10 +68,8 @@ defmodule Pleroma.Web.Federator do
# Job Worker Callbacks
- @spec perform(atom(), module(), any()) :: {:ok, any()} | {:error, any()}
- def perform(:publish_one, module, params) do
- apply(module, :publish_one, [params])
- end
+ @spec perform(atom(), any()) :: {:ok, any()} | {:error, any()}
+ def perform(:publish_one, params), do: Publisher.publish_one(params)
def perform(:publish, activity) do
Logger.debug(fn -> "Running publish for #{activity.data["id"]}" end)
diff --git a/lib/pleroma/web/federator/publisher.ex b/lib/pleroma/web/federator/publisher.ex
@@ -1,110 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.Federator.Publisher do
- alias Pleroma.Activity
- alias Pleroma.Config
- alias Pleroma.User
- alias Pleroma.Workers.PublisherWorker
-
- require Logger
-
- @moduledoc """
- Defines the contract used by federation implementations to publish messages to
- their peers.
- """
-
- @doc """
- Determine whether an activity can be relayed using the federation module.
- """
- @callback is_representable?(Pleroma.Activity.t()) :: boolean()
-
- @doc """
- Relays an activity to a specified peer, determined by the parameters. The
- parameters used are controlled by the federation module.
- """
- @callback publish_one(Map.t()) :: {:ok, Map.t()} | {:error, any()}
-
- @doc """
- Enqueue publishing a single activity.
- """
- @spec enqueue_one(module(), Map.t(), Keyword.t()) :: {:ok, %Oban.Job{}}
- def enqueue_one(module, %{} = params, worker_args \\ []) do
- PublisherWorker.enqueue(
- "publish_one",
- %{"module" => to_string(module), "params" => params},
- worker_args
- )
- end
-
- @doc """
- Relays an activity to all specified peers.
- """
- @callback publish(User.t(), Activity.t()) :: :ok | {:error, any()}
-
- @spec publish(User.t(), Activity.t()) :: :ok
- def publish(%User{} = user, %Activity{} = activity) do
- Config.get([:instance, :federation_publisher_modules])
- |> Enum.each(fn module ->
- if module.is_representable?(activity) do
- Logger.debug("Publishing #{activity.data["id"]} using #{inspect(module)}")
- module.publish(user, activity)
- end
- end)
-
- :ok
- end
-
- @doc """
- Gathers links used by an outgoing federation module for WebFinger output.
- """
- @callback gather_webfinger_links(User.t()) :: list()
-
- @spec gather_webfinger_links(User.t()) :: list()
- def gather_webfinger_links(%User{} = user) do
- Config.get([:instance, :federation_publisher_modules])
- |> Enum.reduce([], fn module, links ->
- links ++ module.gather_webfinger_links(user)
- end)
- end
-
- @doc """
- Gathers nodeinfo protocol names supported by the federation module.
- """
- @callback gather_nodeinfo_protocol_names() :: list()
-
- @spec gather_nodeinfo_protocol_names() :: list()
- def gather_nodeinfo_protocol_names do
- Config.get([:instance, :federation_publisher_modules])
- |> Enum.reduce([], fn module, links ->
- links ++ module.gather_nodeinfo_protocol_names()
- end)
- end
-
- @doc """
- Gathers a set of remote users given an IR envelope.
- """
- def remote_users(%User{id: user_id}, %{data: %{"to" => to} = data}) do
- cc = Map.get(data, "cc", [])
-
- bcc =
- data
- |> Map.get("bcc", [])
- |> Enum.reduce([], fn ap_id, bcc ->
- case Pleroma.List.get_by_ap_id(ap_id) do
- %Pleroma.List{user_id: ^user_id} = list ->
- {:ok, following} = Pleroma.List.get_following(list)
- bcc ++ Enum.map(following, & &1.ap_id)
-
- _ ->
- bcc
- end
- end)
-
- [to, cc, bcc]
- |> Enum.concat()
- |> Enum.map(&User.get_cached_by_ap_id/1)
- |> Enum.filter(fn user -> user && !user.local end)
- end
-end
diff --git a/lib/pleroma/web/feed/feed_view.ex b/lib/pleroma/web/feed/feed_view.ex
@@ -132,7 +132,7 @@ defmodule Pleroma.Web.Feed.FeedView do
|> safe_to_string()
end
- @spec to_rfc3339(String.t() | NativeDateTime.t()) :: String.t()
+ @spec to_rfc3339(String.t() | NaiveDateTime.t()) :: String.t()
def to_rfc3339(date) when is_binary(date) do
date
|> Timex.parse!("{ISO:Extended}")
@@ -145,7 +145,7 @@ defmodule Pleroma.Web.Feed.FeedView do
|> Timex.format!("{RFC3339}")
end
- @spec to_rfc2822(String.t() | DateTime.t() | NativeDateTime.t()) :: String.t()
+ @spec to_rfc2822(String.t() | DateTime.t() | NaiveDateTime.t()) :: String.t()
def to_rfc2822(datestr) when is_binary(datestr) do
datestr
|> Timex.parse!("{ISO:Extended}")
diff --git a/lib/pleroma/web/gettext.ex b/lib/pleroma/web/gettext.ex
@@ -85,12 +85,12 @@ defmodule Pleroma.Web.Gettext do
Process.get({Pleroma.Web.Gettext, :locales}, [])
end
- def is_locale_list(locales) do
+ def locale_list?(locales) do
Enum.all?(locales, &is_binary/1)
end
def put_locales(locales) do
- if is_locale_list(locales) do
+ if locale_list?(locales) do
Process.put({Pleroma.Web.Gettext, :locales}, Enum.uniq(locales))
Gettext.put_locale(Enum.at(locales, 0, Gettext.get_locale()))
:ok
diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
@@ -30,7 +30,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
alias Pleroma.Web.TwitterAPI.TwitterAPI
alias Pleroma.Web.Utils.Params
- plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
plug(:skip_auth when action in [:create, :lookup])
@@ -72,7 +72,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
%{scopes: ["follow", "write:blocks"]} when action in [:block, :unblock]
)
- plug(OAuthScopesPlug, %{scopes: ["read:follows"]} when action == :relationships)
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["read:follows"]} when action in [:relationships, :familiar_followers]
+ )
plug(
OAuthScopesPlug,
@@ -92,7 +95,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
plug(
RateLimiter,
- [name: :relation_id_action, params: [:id, :uri]] when action in @relationship_actions
+ [name: :relation_id_action, params: ["id", "uri"]] when action in @relationship_actions
)
plug(RateLimiter, [name: :relations_actions] when action in @relationship_actions)
@@ -104,7 +107,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.AccountOperation
@doc "POST /api/v1/accounts"
- def create(%{assigns: %{app: app}, body_params: params} = conn, _params) do
+ def create(
+ %{assigns: %{app: app}, private: %{open_api_spex: %{body_params: params}}} = conn,
+ _params
+ ) do
with :ok <- validate_email_param(params),
:ok <- TwitterAPI.validate_captcha(app, params),
{:ok, user} <- TwitterAPI.register_user(params),
@@ -168,7 +174,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
end
@doc "PATCH /api/v1/accounts/update_credentials"
- def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _params) do
+ def update_credentials(
+ %{assigns: %{user: user}, private: %{open_api_spex: %{body_params: params}}} = conn,
+ _params
+ ) do
params =
params
|> Enum.filter(fn {_, value} -> not is_nil(value) end)
@@ -235,7 +244,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
# So we first build the normal local changeset, then apply it to the
# user data, but don't persist it. With this, we generate the object
# data for our update activity. We feed this and the changeset as meta
- # inforation into the pipeline, where they will be properly updated and
+ # information into the pipeline, where they will be properly updated and
# federated.
with changeset <- User.update_changeset(user, user_params),
{:ok, unpersisted_user} <- Ecto.Changeset.apply_action(changeset, :update),
@@ -289,7 +298,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
end
@doc "GET /api/v1/accounts/relationships"
- def relationships(%{assigns: %{user: user}} = conn, %{id: id}) do
+ def relationships(
+ %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn,
+ _
+ ) do
targets = User.get_all_by_ids(List.wrap(id))
render(conn, "relationships.json", user: user, targets: targets)
@@ -299,7 +311,13 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
@doc "GET /api/v1/accounts/:id"
- def show(%{assigns: %{user: for_user}} = conn, %{id: nickname_or_id} = params) do
+ def show(
+ %{
+ assigns: %{user: for_user},
+ private: %{open_api_spex: %{params: %{id: nickname_or_id} = params}}
+ } = conn,
+ _params
+ ) do
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
:visible <- User.visible_for(user, for_user) do
render(conn, "show.json",
@@ -313,7 +331,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
end
@doc "GET /api/v1/accounts/:id/statuses"
- def statuses(%{assigns: %{user: reading_user}} = conn, params) do
+ def statuses(
+ %{assigns: %{user: reading_user}, private: %{open_api_spex: %{params: params}}} = conn,
+ _params
+ ) do
with %User{} = user <- User.get_cached_by_nickname_or_id(params.id, for: reading_user),
:visible <- User.visible_for(user, reading_user) do
params =
@@ -348,7 +369,11 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
end
@doc "GET /api/v1/accounts/:id/followers"
- def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
+ def followers(
+ %{assigns: %{user: for_user, account: user}, private: %{open_api_spex: %{params: params}}} =
+ conn,
+ _params
+ ) do
params =
params
|> Enum.map(fn {key, value} -> {to_string(key), value} end)
@@ -373,7 +398,11 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
end
@doc "GET /api/v1/accounts/:id/following"
- def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
+ def following(
+ %{assigns: %{user: for_user, account: user}, private: %{open_api_spex: %{params: params}}} =
+ conn,
+ _params
+ ) do
params =
params
|> Enum.map(fn {key, value} -> {to_string(key), value} end)
@@ -411,7 +440,13 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
{:error, "Can not follow yourself"}
end
- def follow(%{body_params: params, assigns: %{user: follower, account: followed}} = conn, _) do
+ def follow(
+ %{
+ assigns: %{user: follower, account: followed},
+ private: %{open_api_spex: %{body_params: params}}
+ } = conn,
+ _
+ ) do
with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
render(conn, "relationship.json", user: follower, target: followed)
else
@@ -431,7 +466,13 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
end
@doc "POST /api/v1/accounts/:id/mute"
- def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
+ def mute(
+ %{
+ assigns: %{user: muter, account: muted},
+ private: %{open_api_spex: %{body_params: params}}
+ } = conn,
+ _params
+ ) do
params =
params
|> Map.put_new(:duration, Map.get(params, :expires_in, 0))
@@ -472,7 +513,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
@doc "POST /api/v1/accounts/:id/note"
def note(
- %{assigns: %{user: noter, account: target}, body_params: %{comment: comment}} = conn,
+ %{
+ assigns: %{user: noter, account: target},
+ private: %{open_api_spex: %{body_params: %{comment: comment}}}
+ } = conn,
_params
) do
with {:ok, _user_note} <- UserNote.create(noter, target, comment) do
@@ -513,7 +557,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
end
@doc "POST /api/v1/follows"
- def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do
+ def follow_by_uri(%{private: %{open_api_spex: %{body_params: %{uri: uri}}}} = conn, _) do
case User.get_cached_by_nickname(uri) do
%User{} = user ->
conn
@@ -561,7 +605,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
end
@doc "GET /api/v1/accounts/lookup"
- def lookup(conn, %{acct: nickname} = _params) do
+ def lookup(%{private: %{open_api_spex: %{params: %{acct: nickname}}}} = conn, _params) do
with %User{} = user <- User.get_by_nickname(nickname) do
render(conn, "show.json",
user: user,
@@ -588,6 +632,35 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
)
end
+ @doc "GET /api/v1/accounts/familiar_followers"
+ def familiar_followers(
+ %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn,
+ _id
+ ) do
+ users =
+ User.get_all_by_ids(List.wrap(id))
+ |> Enum.map(&%{id: &1.id, accounts: get_familiar_followers(&1, user)})
+
+ conn
+ |> render("familiar_followers.json",
+ for: user,
+ users: users,
+ as: :user
+ )
+ end
+
+ defp get_familiar_followers(%{id: id} = user, %{id: id}) do
+ User.get_familiar_followers(user, user)
+ end
+
+ defp get_familiar_followers(%{hide_followers: true}, _current_user) do
+ []
+ end
+
+ defp get_familiar_followers(user, current_user) do
+ User.get_familiar_followers(user, current_user)
+ end
+
@doc "GET /api/v1/identity_proofs"
def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/directory_controller.ex b/lib/pleroma/web/mastodon_api/controllers/directory_controller.ex
@@ -15,7 +15,7 @@ defmodule Pleroma.Web.MastodonAPI.DirectoryController do
plug(Pleroma.Web.ApiSpec.CastAndValidate)
- plug(:skip_auth when action == "index")
+ plug(:skip_auth when action == :index)
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.DirectoryOperation
diff --git a/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex b/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex
@@ -8,7 +8,7 @@ defmodule Pleroma.Web.MastodonAPI.DomainBlockController do
alias Pleroma.User
alias Pleroma.Web.Plugs.OAuthScopesPlug
- plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.DomainBlockOperation
plug(
@@ -27,23 +27,31 @@ defmodule Pleroma.Web.MastodonAPI.DomainBlockController do
end
@doc "POST /api/v1/domain_blocks"
- def create(%{assigns: %{user: blocker}, body_params: %{domain: domain}} = conn, _params) do
+ def create(
+ %{assigns: %{user: blocker}, private: %{open_api_spex: %{body_params: %{domain: domain}}}} =
+ conn,
+ _params
+ ) do
User.block_domain(blocker, domain)
json(conn, %{})
end
- def create(%{assigns: %{user: blocker}} = conn, %{domain: domain}) do
+ def create(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
User.block_domain(blocker, domain)
json(conn, %{})
end
@doc "DELETE /api/v1/domain_blocks"
- def delete(%{assigns: %{user: blocker}, body_params: %{domain: domain}} = conn, _params) do
+ def delete(
+ %{assigns: %{user: blocker}, private: %{open_api_spex: %{body_params: %{domain: domain}}}} =
+ conn,
+ _params
+ ) do
User.unblock_domain(blocker, domain)
json(conn, %{})
end
- def delete(%{assigns: %{user: blocker}} = conn, %{domain: domain}) do
+ def delete(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
User.unblock_domain(blocker, domain)
json(conn, %{})
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex b/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex
@@ -9,7 +9,7 @@ defmodule Pleroma.Web.MastodonAPI.FollowRequestController do
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Plugs.OAuthScopesPlug
- plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
plug(:assign_follower when action != :index)
action_fallback(:errors)
@@ -44,7 +44,7 @@ defmodule Pleroma.Web.MastodonAPI.FollowRequestController do
end
end
- defp assign_follower(%{params: %{id: id}} = conn, _) do
+ defp assign_follower(%{private: %{open_api_spex: %{params: %{id: id}}}} = conn, _) do
case User.get_cached_by_id(id) do
%User{} = follower -> assign(conn, :follower, follower)
nil -> Pleroma.Web.MastodonAPI.FallbackController.call(conn, {:error, :not_found}) |> halt()
diff --git a/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex b/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex
@@ -7,7 +7,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceController do
plug(Pleroma.Web.ApiSpec.CastAndValidate)
- plug(:skip_auth when action in [:show, :peers])
+ plug(:skip_auth when action in [:show, :show2, :peers])
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.InstanceOperation
@@ -16,8 +16,18 @@ defmodule Pleroma.Web.MastodonAPI.InstanceController do
render(conn, "show.json")
end
+ @doc "GET /api/v2/instance"
+ def show2(conn, _params) do
+ render(conn, "show2.json")
+ end
+
@doc "GET /api/v1/instance/peers"
def peers(conn, _params) do
json(conn, Pleroma.Stats.get_peers())
end
+
+ @doc "GET /api/v1/instance/rules"
+ def rules(conn, _params) do
+ render(conn, "rules.json")
+ end
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/list_controller.ex b/lib/pleroma/web/mastodon_api/controllers/list_controller.ex
@@ -11,7 +11,7 @@ defmodule Pleroma.Web.MastodonAPI.ListController do
@oauth_read_actions [:index, :show, :list_accounts]
- plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
plug(:list_by_id_and_user when action not in [:index, :create])
plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action in @oauth_read_actions)
plug(OAuthScopesPlug, %{scopes: ["write:lists"]} when action not in @oauth_read_actions)
@@ -21,25 +21,33 @@ defmodule Pleroma.Web.MastodonAPI.ListController do
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.ListOperation
# GET /api/v1/lists
- def index(%{assigns: %{user: user}} = conn, opts) do
- lists = Pleroma.List.for_user(user, opts)
+ def index(%{assigns: %{user: user}, private: %{open_api_spex: %{params: params}}} = conn, _) do
+ lists = Pleroma.List.for_user(user, params)
render(conn, "index.json", lists: lists)
end
# POST /api/v1/lists
- def create(%{assigns: %{user: user}, body_params: %{title: title}} = conn, _) do
+ def create(
+ %{assigns: %{user: user}, private: %{open_api_spex: %{body_params: %{title: title}}}} =
+ conn,
+ _
+ ) do
with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
render(conn, "show.json", list: list)
end
end
- # GET /api/v1/lists/:id
+ # GET /api/v1/lists/:idOB
def show(%{assigns: %{list: list}} = conn, _) do
render(conn, "show.json", list: list)
end
# PUT /api/v1/lists/:id
- def update(%{assigns: %{list: list}, body_params: %{title: title}} = conn, _) do
+ def update(
+ %{assigns: %{list: list}, private: %{open_api_spex: %{body_params: %{title: title}}}} =
+ conn,
+ _
+ ) do
with {:ok, list} <- Pleroma.List.rename(list, title) do
render(conn, "show.json", list: list)
end
@@ -62,7 +70,13 @@ defmodule Pleroma.Web.MastodonAPI.ListController do
end
# POST /api/v1/lists/:id/accounts
- def add_to_list(%{assigns: %{list: list}, body_params: %{account_ids: account_ids}} = conn, _) do
+ def add_to_list(
+ %{
+ assigns: %{list: list},
+ private: %{open_api_spex: %{body_params: %{account_ids: account_ids}}}
+ } = conn,
+ _
+ ) do
Enum.each(account_ids, fn account_id ->
with %User{} = followed <- User.get_cached_by_id(account_id) do
Pleroma.List.follow(list, followed)
@@ -74,9 +88,22 @@ defmodule Pleroma.Web.MastodonAPI.ListController do
# DELETE /api/v1/lists/:id/accounts
def remove_from_list(
- %{assigns: %{list: list}, params: %{account_ids: account_ids}} = conn,
+ %{
+ private: %{open_api_spex: %{params: %{account_ids: account_ids}}}
+ } = conn,
_
) do
+ do_remove_from_list(conn, account_ids)
+ end
+
+ def remove_from_list(
+ %{private: %{open_api_spex: %{body_params: %{account_ids: account_ids}}}} = conn,
+ _
+ ) do
+ do_remove_from_list(conn, account_ids)
+ end
+
+ defp do_remove_from_list(%{assigns: %{list: list}} = conn, account_ids) do
Enum.each(account_ids, fn account_id ->
with %User{} = followed <- User.get_cached_by_id(account_id) do
Pleroma.List.unfollow(list, followed)
@@ -86,11 +113,10 @@ defmodule Pleroma.Web.MastodonAPI.ListController do
json(conn, %{})
end
- def remove_from_list(%{body_params: params} = conn, _) do
- remove_from_list(%{conn | params: params}, %{})
- end
-
- defp list_by_id_and_user(%{assigns: %{user: user}, params: %{id: id}} = conn, _) do
+ defp list_by_id_and_user(
+ %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn,
+ _
+ ) do
case Pleroma.List.get(id, user) do
%Pleroma.List{} = list -> assign(conn, :list, list)
nil -> conn |> render_error(:not_found, "List not found") |> halt()
diff --git a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex
@@ -12,7 +12,7 @@ defmodule Pleroma.Web.MastodonAPI.MediaController do
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
plug(Majic.Plug, [pool: Pleroma.MajicPool] when action in [:create, :create2])
- plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
plug(OAuthScopesPlug, %{scopes: ["read:media"]} when action == :show)
plug(OAuthScopesPlug, %{scopes: ["write:media"]} when action != :show)
@@ -20,7 +20,11 @@ defmodule Pleroma.Web.MastodonAPI.MediaController do
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.MediaOperation
@doc "POST /api/v1/media"
- def create(%{assigns: %{user: user}, body_params: %{file: file} = data} = conn, _) do
+ def create(
+ %{assigns: %{user: user}, private: %{open_api_spex: %{body_params: %{file: file} = data}}} =
+ conn,
+ _
+ ) do
with {:ok, object} <-
ActivityPub.upload(
file,
@@ -36,7 +40,11 @@ defmodule Pleroma.Web.MastodonAPI.MediaController do
def create(_conn, _data), do: {:error, :bad_request}
@doc "POST /api/v2/media"
- def create2(%{assigns: %{user: user}, body_params: %{file: file} = data} = conn, _) do
+ def create2(
+ %{assigns: %{user: user}, private: %{open_api_spex: %{body_params: %{file: file} = data}}} =
+ conn,
+ _
+ ) do
with {:ok, object} <-
ActivityPub.upload(
file,
@@ -54,7 +62,15 @@ defmodule Pleroma.Web.MastodonAPI.MediaController do
def create2(_conn, _data), do: {:error, :bad_request}
@doc "PUT /api/v1/media/:id"
- def update(%{assigns: %{user: user}, body_params: %{description: description}} = conn, %{id: id}) do
+ def update(
+ %{
+ assigns: %{user: user},
+ private: %{
+ open_api_spex: %{body_params: %{description: description}, params: %{id: id}}
+ }
+ } = conn,
+ _
+ ) do
with %Object{} = object <- Object.get_by_id(id),
:ok <- Object.authorize_access(object, user),
{:ok, %Object{data: data}} <- Object.update_data(object, %{"name" => description}) do
@@ -67,7 +83,7 @@ defmodule Pleroma.Web.MastodonAPI.MediaController do
def update(conn, data), do: show(conn, data)
@doc "GET /api/v1/media/:id"
- def show(%{assigns: %{user: user}} = conn, %{id: id}) do
+ def show(%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn, _) do
with %Object{data: data, id: object_id} = object <- Object.get_by_id(id),
:ok <- Object.authorize_access(object, user) do
attachment_data = Map.put(data, "id", object_id)
diff --git a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex
@@ -13,7 +13,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do
@oauth_read_actions [:show, :index]
- plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
plug(
OAuthScopesPlug,
@@ -24,8 +24,20 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.NotificationOperation
+ @default_notification_types ~w{
+ mention
+ follow
+ follow_request
+ reblog
+ favourite
+ move
+ pleroma:emoji_reaction
+ poll
+ update
+ }
+
# GET /api/v1/notifications
- def index(conn, %{account_id: account_id} = params) do
+ def index(%{private: %{open_api_spex: %{params: %{account_id: account_id} = params}}} = conn, _) do
case Pleroma.User.get_cached_by_id(account_id) do
%{ap_id: account_ap_id} ->
params =
@@ -33,7 +45,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do
|> Map.delete(:account_id)
|> Map.put(:account_ap_id, account_ap_id)
- index(conn, params)
+ do_get_notifications(conn, params)
_ ->
conn
@@ -42,18 +54,11 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do
end
end
- @default_notification_types ~w{
- mention
- follow
- follow_request
- reblog
- favourite
- move
- pleroma:emoji_reaction
- poll
- update
- }
- def index(%{assigns: %{user: user}} = conn, params) do
+ def index(%{private: %{open_api_spex: %{params: params}}} = conn, _) do
+ do_get_notifications(conn, params)
+ end
+
+ defp do_get_notifications(%{assigns: %{user: user}} = conn, params) do
params =
Map.new(params, fn {k, v} -> {to_string(k), v} end)
|> Map.put_new("types", Map.get(params, :include_types, @default_notification_types))
@@ -69,7 +74,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do
end
# GET /api/v1/notifications/:id
- def show(%{assigns: %{user: user}} = conn, %{id: id}) do
+ def show(%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn, _) do
with {:ok, notification} <- Notification.get(user, id) do
render(conn, "show.json", notification: notification, for: user)
else
@@ -88,8 +93,20 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do
# POST /api/v1/notifications/:id/dismiss
- def dismiss(%{assigns: %{user: user}} = conn, %{id: id} = _params) do
- with {:ok, _notif} <- Notification.dismiss(user, id) do
+ def dismiss(%{private: %{open_api_spex: %{params: %{id: id}}}} = conn, _) do
+ do_dismiss(conn, id)
+ end
+
+ # POST /api/v1/notifications/dismiss (deprecated)
+ def dismiss_via_body(
+ %{private: %{open_api_spex: %{body_params: %{id: id}}}} = conn,
+ _
+ ) do
+ do_dismiss(conn, id)
+ end
+
+ defp do_dismiss(%{assigns: %{user: user}} = conn, notification_id) do
+ with {:ok, _notif} <- Notification.dismiss(user, notification_id) do
json(conn, %{})
else
{:error, reason} ->
@@ -99,13 +116,11 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do
end
end
- # POST /api/v1/notifications/dismiss (deprecated)
- def dismiss_via_body(%{body_params: params} = conn, _) do
- dismiss(conn, params)
- end
-
# DELETE /api/v1/notifications/destroy_multiple
- def destroy_multiple(%{assigns: %{user: user}} = conn, %{ids: ids} = _params) do
+ def destroy_multiple(
+ %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{ids: ids}}}} = conn,
+ _
+ ) do
Notification.destroy_multiple(user, ids)
json(conn, %{})
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex b/lib/pleroma/web/mastodon_api/controllers/poll_controller.ex
@@ -15,7 +15,7 @@ defmodule Pleroma.Web.MastodonAPI.PollController do
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
- plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
plug(
OAuthScopesPlug,
@@ -29,7 +29,7 @@ defmodule Pleroma.Web.MastodonAPI.PollController do
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
@doc "GET /api/v1/polls/:id"
- def show(%{assigns: %{user: user}} = conn, %{id: id}) do
+ def show(%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn, _) do
with %Object{} = object <- Object.get_by_id_and_maybe_refetch(id, interval: 60),
%Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
true <- Visibility.visible_for_user?(activity, user) do
@@ -41,7 +41,13 @@ defmodule Pleroma.Web.MastodonAPI.PollController do
end
@doc "POST /api/v1/polls/:id/votes"
- def vote(%{assigns: %{user: user}, body_params: %{choices: choices}} = conn, %{id: id}) do
+ def vote(
+ %{
+ assigns: %{user: user},
+ private: %{open_api_spex: %{body_params: %{choices: choices}, params: %{id: id}}}
+ } = conn,
+ _
+ ) do
with %Object{data: %{"type" => "Question"}} = object <- Object.get_by_id(id),
%Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
true <- Visibility.visible_for_user?(activity, user),
diff --git a/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex b/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex
@@ -13,7 +13,7 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityController do
@oauth_read_actions [:show, :index]
- plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action in @oauth_read_actions)
plug(OAuthScopesPlug, %{scopes: ["write:statuses"]} when action not in @oauth_read_actions)
plug(:assign_scheduled_activity when action != :index)
@@ -23,7 +23,7 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityController do
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.ScheduledActivityOperation
@doc "GET /api/v1/scheduled_statuses"
- def index(%{assigns: %{user: user}} = conn, params) do
+ def index(%{assigns: %{user: user}, private: %{open_api_spex: %{params: params}}} = conn, _) do
params = Map.new(params, fn {key, value} -> {to_string(key), value} end)
with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
@@ -39,7 +39,13 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityController do
end
@doc "PUT /api/v1/scheduled_statuses/:id"
- def update(%{assigns: %{scheduled_activity: scheduled_activity}, body_params: params} = conn, _) do
+ def update(
+ %{
+ assigns: %{scheduled_activity: scheduled_activity},
+ private: %{open_api_spex: %{body_params: params}}
+ } = conn,
+ _
+ ) do
with {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
render(conn, "show.json", scheduled_activity: scheduled_activity)
end
@@ -52,7 +58,10 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityController do
end
end
- defp assign_scheduled_activity(%{assigns: %{user: user}, params: %{id: id}} = conn, _) do
+ defp assign_scheduled_activity(
+ %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn,
+ _
+ ) do
case ScheduledActivity.get(user, id) do
%ScheduledActivity{} = activity -> assign(conn, :scheduled_activity, activity)
nil -> Pleroma.Web.MastodonAPI.FallbackController.call(conn, {:error, :not_found}) |> halt()
diff --git a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex
@@ -18,7 +18,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
@search_limit 40
- plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
# Note: Mastodon doesn't allow unauthenticated access (requires read:accounts / read:search)
plug(OAuthScopesPlug, %{scopes: ["read:search"], fallback: :proceed_unauthenticated})
@@ -29,7 +29,11 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.SearchOperation
- def account_search(%{assigns: %{user: user}} = conn, %{q: query} = params) do
+ def account_search(
+ %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{q: query} = params}}} =
+ conn,
+ _
+ ) do
accounts = User.search(query, search_options(params, user))
conn
@@ -44,7 +48,12 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
def search2(conn, params), do: do_search(:v2, conn, params)
def search(conn, params), do: do_search(:v1, conn, params)
- defp do_search(version, %{assigns: %{user: user}} = conn, %{q: query} = params) do
+ defp do_search(
+ version,
+ %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{q: query} = params}}} =
+ conn,
+ _
+ ) do
query = String.trim(query)
options = search_options(params, user)
timeout = Keyword.get(Repo.config(), :timeout, 15_000)
@@ -147,7 +156,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
tags
end
- Pleroma.Pagination.paginate(tags, options)
+ Pleroma.Pagination.paginate_list(tags, options)
end
defp add_joined_tag(tags) do
diff --git a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex
@@ -12,6 +12,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
alias Pleroma.Activity
alias Pleroma.Bookmark
+ alias Pleroma.BookmarkFolder
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.ScheduledActivity
@@ -25,7 +26,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
alias Pleroma.Web.Plugs.OAuthScopesPlug
alias Pleroma.Web.Plugs.RateLimiter
- plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
plug(:skip_public_check when action in [:index, :show])
@@ -37,7 +38,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
when action in [
:index,
:show,
- :card,
:context,
:show_history,
:show_source
@@ -110,7 +110,11 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
`ids` query param is required
"""
- def index(%{assigns: %{user: user}} = conn, %{ids: ids} = params) do
+ def index(
+ %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{ids: ids} = params}}} =
+ conn,
+ _
+ ) do
limit = 100
activities =
@@ -134,7 +138,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
def create(
%{
assigns: %{user: user},
- body_params: %{status: _, scheduled_at: scheduled_at} = params
+ private: %{
+ open_api_spex: %{body_params: %{status: _, scheduled_at: scheduled_at} = params}
+ }
} = conn,
_
)
@@ -156,7 +162,13 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
else
{:far_enough, _} ->
params = Map.drop(params, [:scheduled_at])
- create(%Plug.Conn{conn | body_params: params}, %{})
+
+ put_in(
+ conn,
+ [Access.key(:private), Access.key(:open_api_spex), Access.key(:body_params)],
+ params
+ )
+ |> do_create
error ->
error
@@ -164,7 +176,35 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
# Creates a regular status
- def create(%{assigns: %{user: user}, body_params: %{status: _} = params} = conn, _) do
+ def create(
+ %{
+ private: %{open_api_spex: %{body_params: %{status: _}}}
+ } = conn,
+ _
+ ) do
+ do_create(conn)
+ end
+
+ def create(
+ %{
+ assigns: %{user: _user},
+ private: %{open_api_spex: %{body_params: %{media_ids: _} = params}}
+ } = conn,
+ _
+ ) do
+ params = Map.put(params, :status, "")
+
+ put_in(
+ conn,
+ [Access.key(:private), Access.key(:open_api_spex), Access.key(:body_params)],
+ params
+ )
+ |> do_create
+ end
+
+ defp do_create(
+ %{assigns: %{user: user}, private: %{open_api_spex: %{body_params: params}}} = conn
+ ) do
params =
Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
|> put_application(conn)
@@ -189,13 +229,11 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
end
- def create(%{assigns: %{user: _user}, body_params: %{media_ids: _} = params} = conn, _) do
- params = Map.put(params, :status, "")
- create(%Plug.Conn{conn | body_params: params}, %{})
- end
-
@doc "GET /api/v1/statuses/:id/history"
- def show_history(%{assigns: assigns} = conn, %{id: id} = params) do
+ def show_history(
+ %{assigns: assigns, private: %{open_api_spex: %{params: %{id: id} = params}}} = conn,
+ _
+ ) do
with user = assigns[:user],
%Activity{} = activity <- Activity.get_by_id_with_object(id),
true <- Visibility.visible_for_user?(activity, user) do
@@ -211,7 +249,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "GET /api/v1/statuses/:id/source"
- def show_source(%{assigns: assigns} = conn, %{id: id} = _params) do
+ def show_source(%{assigns: assigns, private: %{open_api_spex: %{params: %{id: id}}}} = conn, _) do
with user = assigns[:user],
%Activity{} = activity <- Activity.get_by_id_with_object(id),
true <- Visibility.visible_for_user?(activity, user) do
@@ -225,7 +263,13 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "PUT /api/v1/statuses/:id"
- def update(%{assigns: %{user: user}, body_params: body_params} = conn, %{id: id} = params) do
+ def update(
+ %{
+ assigns: %{user: user},
+ private: %{open_api_spex: %{body_params: body_params, params: %{id: id} = params}}
+ } = conn,
+ _
+ ) 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"},
@@ -248,7 +292,11 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "GET /api/v1/statuses/:id"
- def show(%{assigns: %{user: user}} = conn, %{id: id} = params) do
+ def show(
+ %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id} = params}}} =
+ conn,
+ _
+ ) do
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
true <- Visibility.visible_for_user?(activity, user) do
try_render(conn, "show.json",
@@ -263,7 +311,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "DELETE /api/v1/statuses/:id"
- def delete(%{assigns: %{user: user}} = conn, %{id: id}) do
+ def delete(%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn, _) do
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
{:ok, %Activity{}} <- CommonAPI.delete(id, user) do
try_render(conn, "show.json",
@@ -278,7 +326,13 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "POST /api/v1/statuses/:id/reblog"
- def reblog(%{assigns: %{user: user}, body_params: params} = conn, %{id: ap_id_or_id}) do
+ def reblog(
+ %{
+ assigns: %{user: user},
+ private: %{open_api_spex: %{body_params: params, params: %{id: ap_id_or_id}}}
+ } = conn,
+ _
+ ) do
with {:ok, announce} <- CommonAPI.repeat(ap_id_or_id, user, params),
%Activity{} = announce <- Activity.normalize(announce.data) do
try_render(conn, "show.json", %{activity: announce, for: user, as: :activity})
@@ -286,7 +340,11 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "POST /api/v1/statuses/:id/unreblog"
- def unreblog(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
+ def unreblog(
+ %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: activity_id}}}} =
+ conn,
+ _
+ ) do
with {:ok, _unannounce} <- CommonAPI.unrepeat(activity_id, user),
%Activity{} = activity <- Activity.get_by_id(activity_id) do
try_render(conn, "show.json", %{activity: activity, for: user, as: :activity})
@@ -294,7 +352,11 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "POST /api/v1/statuses/:id/favourite"
- def favourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
+ def favourite(
+ %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: activity_id}}}} =
+ conn,
+ _
+ ) do
with {:ok, _fav} <- CommonAPI.favorite(user, activity_id),
%Activity{} = activity <- Activity.get_by_id(activity_id) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
@@ -302,7 +364,11 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "POST /api/v1/statuses/:id/unfavourite"
- def unfavourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
+ def unfavourite(
+ %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: activity_id}}}} =
+ conn,
+ _
+ ) do
with {:ok, _unfav} <- CommonAPI.unfavorite(activity_id, user),
%Activity{} = activity <- Activity.get_by_id(activity_id) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
@@ -310,7 +376,11 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "POST /api/v1/statuses/:id/pin"
- def pin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
+ def pin(
+ %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: ap_id_or_id}}}} =
+ conn,
+ _
+ ) do
with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
else
@@ -329,24 +399,43 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "POST /api/v1/statuses/:id/unpin"
- def unpin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
+ def unpin(
+ %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: ap_id_or_id}}}} =
+ conn,
+ _
+ ) do
with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
end
end
@doc "POST /api/v1/statuses/:id/bookmark"
- def bookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
+ def bookmark(
+ %{
+ assigns: %{user: user},
+ private: %{open_api_spex: %{body_params: body_params, params: %{id: id}}}
+ } = conn,
+ _
+ ) do
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
%User{} = user <- User.get_cached_by_nickname(user.nickname),
true <- Visibility.visible_for_user?(activity, user),
- {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
+ folder_id <- Map.get(body_params, :folder_id, nil),
+ folder_id <-
+ if(folder_id && BookmarkFolder.belongs_to_user?(folder_id, user.id),
+ do: folder_id,
+ else: nil
+ ),
+ {:ok, _bookmark} <- Bookmark.create(user.id, activity.id, folder_id) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
end
end
@doc "POST /api/v1/statuses/:id/unbookmark"
- def unbookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
+ def unbookmark(
+ %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn,
+ _
+ ) do
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
%User{} = user <- User.get_cached_by_nickname(user.nickname),
true <- Visibility.visible_for_user?(activity, user),
@@ -356,7 +445,13 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "POST /api/v1/statuses/:id/mute"
- def mute_conversation(%{assigns: %{user: user}, body_params: params} = conn, %{id: id}) do
+ def mute_conversation(
+ %{
+ assigns: %{user: user},
+ private: %{open_api_spex: %{body_params: params, params: %{id: id}}}
+ } = conn,
+ _
+ ) do
with %Activity{} = activity <- Activity.get_by_id(id),
{:ok, activity} <- CommonAPI.add_mute(user, activity, params) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
@@ -364,27 +459,24 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "POST /api/v1/statuses/:id/unmute"
- def unmute_conversation(%{assigns: %{user: user}} = conn, %{id: id}) do
+ def unmute_conversation(
+ %{
+ assigns: %{user: user},
+ private: %{open_api_spex: %{params: %{id: id}}}
+ } = conn,
+ _
+ ) do
with %Activity{} = activity <- Activity.get_by_id(id),
{:ok, activity} <- CommonAPI.remove_mute(user, activity) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
end
end
- @doc "GET /api/v1/statuses/:id/card"
- @deprecated "https://github.com/tootsuite/mastodon/pull/11213"
- def card(%{assigns: %{user: user}} = conn, %{id: status_id}) do
- with %Activity{} = activity <- Activity.get_by_id(status_id),
- true <- Visibility.visible_for_user?(activity, user) do
- data = Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
- render(conn, "card.json", data)
- else
- _ -> render_error(conn, :not_found, "Record not found")
- end
- end
-
@doc "GET /api/v1/statuses/:id/favourited_by"
- def favourited_by(%{assigns: %{user: user}} = conn, %{id: id}) do
+ def favourited_by(
+ %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn,
+ _
+ ) do
with true <- Pleroma.Config.get([:instance, :show_reactions]),
%Activity{} = activity <- Activity.get_by_id_with_object(id),
{:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
@@ -405,7 +497,10 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "GET /api/v1/statuses/:id/reblogged_by"
- def reblogged_by(%{assigns: %{user: user}} = conn, %{id: id}) do
+ def reblogged_by(
+ %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn,
+ _
+ ) do
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
{:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
%Object{data: %{"announcements" => announces, "id" => ap_id}} <-
@@ -437,7 +532,10 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "GET /api/v1/statuses/:id/context"
- def context(%{assigns: %{user: user}} = conn, %{id: id}) do
+ def context(
+ %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn,
+ _
+ ) do
with %Activity{} = activity <- Activity.get_by_id(id) do
activities =
ActivityPub.fetch_activities_for_context(activity.data["context"], %{
@@ -451,7 +549,10 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "GET /api/v1/favourites"
- def favourites(%{assigns: %{user: %User{} = user}} = conn, params) do
+ def favourites(
+ %{assigns: %{user: %User{} = user}, private: %{open_api_spex: %{params: params}}} = conn,
+ _
+ ) do
activities = ActivityPub.fetch_favourites(user, params)
conn
@@ -464,12 +565,13 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "GET /api/v1/bookmarks"
- def bookmarks(%{assigns: %{user: user}} = conn, params) do
+ def bookmarks(%{assigns: %{user: user}, private: %{open_api_spex: %{params: params}}} = conn, _) do
user = User.get_cached_by_id(user.id)
+ folder_id = Map.get(params, :folder_id)
bookmarks =
user.id
- |> Bookmark.for_user_query()
+ |> Bookmark.for_user_query(folder_id)
|> Pleroma.Pagination.fetch_paginated(params)
activities =
diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex
@@ -193,7 +193,28 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
render_many(targets, AccountView, "relationship.json", render_opts)
end
+ def render("familiar_followers.json", %{users: users} = opts) do
+ opts =
+ opts
+ |> Map.merge(%{as: :user})
+ |> Map.delete(:users)
+
+ users
+ |> render_many(AccountView, "familiar_followers.json", opts)
+ end
+
+ def render("familiar_followers.json", %{user: %{id: id, accounts: accounts}} = opts) do
+ accounts =
+ accounts
+ |> render_many(AccountView, "show.json", opts)
+ |> Enum.filter(&Enum.any?/1)
+
+ %{id: id, accounts: accounts}
+ end
+
defp do_render("show.json", %{user: user} = opts) do
+ self = opts[:for] == user
+
user = User.sanitize_html(user, User.html_filter_policy(opts[:for]))
display_name = user.name || user.nickname
@@ -203,16 +224,16 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
header_static = User.banner_url(user) |> MediaProxy.preview_url(static: true)
following_count =
- if !user.hide_follows_count or !user.hide_follows or opts[:for] == user,
+ if !user.hide_follows_count or !user.hide_follows or self,
do: user.following_count,
else: 0
followers_count =
- if !user.hide_followers_count or !user.hide_followers or opts[:for] == user,
+ if !user.hide_followers_count or !user.hide_followers or self,
do: user.follower_count,
else: 0
- bot = user.actor_type == "Service"
+ bot = bot?(user)
emojis =
Enum.map(user.emoji, fn {shortcode, raw_url} ->
@@ -468,4 +489,12 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
defp image_url(%{"url" => [%{"href" => href} | _]}), do: href
defp image_url(_), do: nil
+
+ defp bot?(user) do
+ # Because older and/or Mastodon clients may not recognize a Group actor properly,
+ # and currently the group actor can only boost things, we should let these clients
+ # think groups are bots.
+ # See https://git.pleroma.social/pleroma/pleroma-meta/-/issues/14
+ user.actor_type == "Service" || user.actor_type == "Group"
+ end
end
diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex
@@ -13,12 +13,11 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
def render("show.json", _) do
instance = Config.get(:instance)
- %{
- uri: Pleroma.Web.Endpoint.url(),
- title: Keyword.get(instance, :name),
+ common_information(instance)
+ |> Map.merge(%{
+ uri: Pleroma.Web.WebFinger.host(),
description: Keyword.get(instance, :description),
short_description: Keyword.get(instance, :short_description),
- version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
email: Keyword.get(instance, :email),
urls: %{
streaming_api: Pleroma.Web.Endpoint.websocket_url()
@@ -27,9 +26,10 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
thumbnail:
URI.merge(Pleroma.Web.Endpoint.url(), Keyword.get(instance, :instance_thumbnail))
|> to_string,
- languages: Keyword.get(instance, :languages, ["en"]),
registrations: Keyword.get(instance, :registrations_open),
approval_required: Keyword.get(instance, :account_approval_required),
+ contact_account: contact_account(Keyword.get(instance, :contact_username)),
+ configuration: configuration(),
# Extra (not present in Mastodon):
max_toot_chars: Keyword.get(instance, :limit),
max_media_attachments: Keyword.get(instance, :max_media_attachments),
@@ -41,19 +41,61 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
background_image: Pleroma.Web.Endpoint.url() <> Keyword.get(instance, :background_image),
shout_limit: Config.get([:shout, :limit]),
description_limit: Keyword.get(instance, :description_limit),
- pleroma: %{
- metadata: %{
- account_activation_required: Keyword.get(instance, :account_activation_required),
- features: features(),
- federation: federation(),
- fields_limits: fields_limits(),
- post_formats: Config.get([:instance, :allowed_post_formats]),
- birthday_required: Config.get([:instance, :birthday_required]),
- birthday_min_age: Config.get([:instance, :birthday_min_age])
- },
- stats: %{mau: Pleroma.User.active_user_count()},
- vapid_public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key)
- }
+ chat_limit: Keyword.get(instance, :chat_limit),
+ pleroma: pleroma_configuration(instance)
+ })
+ end
+
+ def render("show2.json", _) do
+ instance = Config.get(:instance)
+
+ common_information(instance)
+ |> Map.merge(%{
+ domain: Pleroma.Web.WebFinger.host(),
+ source_url: Pleroma.Application.repository(),
+ description: Keyword.get(instance, :short_description),
+ usage: %{users: %{active_month: Pleroma.User.active_user_count()}},
+ thumbnail: %{
+ url:
+ URI.merge(Pleroma.Web.Endpoint.url(), Keyword.get(instance, :instance_thumbnail))
+ |> to_string
+ },
+ configuration: configuration2(),
+ registrations: %{
+ enabled: Keyword.get(instance, :registrations_open),
+ approval_required: Keyword.get(instance, :account_approval_required),
+ message: nil,
+ url: nil
+ },
+ contact: %{
+ email: Keyword.get(instance, :email),
+ account: contact_account(Keyword.get(instance, :contact_username))
+ },
+ # Extra (not present in Mastodon):
+ pleroma: pleroma_configuration2(instance)
+ })
+ end
+
+ def render("rules.json", _) do
+ Pleroma.Rule.query()
+ |> Pleroma.Repo.all()
+ |> render_many(__MODULE__, "rule.json", as: :rule)
+ end
+
+ def render("rule.json", %{rule: rule}) do
+ %{
+ id: to_string(rule.id),
+ text: rule.text,
+ hint: rule.hint || ""
+ }
+ end
+
+ defp common_information(instance) do
+ %{
+ languages: Keyword.get(instance, :languages, ["en"]),
+ rules: render(__MODULE__, "rules.json"),
+ title: Keyword.get(instance, :name),
+ version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})"
}
end
@@ -101,7 +143,9 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
if Config.get([:instance, :profile_directory]) do
"profile_directory"
end,
- "pleroma:get:main/ostatus"
+ "pleroma:get:main/ostatus",
+ "pleroma:group_actors",
+ "pleroma:bookmark_folders"
]
|> Enum.filter(& &1)
end
@@ -133,7 +177,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
|> Map.put(:enabled, Config.get([:instance, :federating]))
end
- def fields_limits do
+ defp fields_limits do
%{
max_fields: Config.get([:instance, :max_account_fields]),
max_remote_fields: Config.get([:instance, :max_remote_account_fields]),
@@ -141,4 +185,94 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
value_length: Config.get([:instance, :account_field_value_length])
}
end
+
+ defp contact_account(nil), do: nil
+
+ defp contact_account("@" <> username) do
+ contact_account(username)
+ end
+
+ defp contact_account(username) do
+ user = Pleroma.User.get_cached_by_nickname(username)
+
+ if user do
+ Pleroma.Web.MastodonAPI.AccountView.render("show.json", %{user: user, for: nil})
+ else
+ nil
+ end
+ end
+
+ defp configuration do
+ %{
+ accounts: %{
+ max_featured_tags: 0
+ },
+ statuses: %{
+ max_characters: Config.get([:instance, :limit]),
+ max_media_attachments: Config.get([:instance, :max_media_attachments])
+ },
+ media_attachments: %{
+ image_size_limit: Config.get([:instance, :upload_limit]),
+ video_size_limit: Config.get([:instance, :upload_limit]),
+ supported_mime_types: ["application/octet-stream"]
+ },
+ polls: %{
+ max_options: Config.get([:instance, :poll_limits, :max_options]),
+ max_characters_per_option: Config.get([:instance, :poll_limits, :max_option_chars]),
+ min_expiration: Config.get([:instance, :poll_limits, :min_expiration]),
+ max_expiration: Config.get([:instance, :poll_limits, :max_expiration])
+ }
+ }
+ end
+
+ defp configuration2 do
+ configuration()
+ |> put_in([:accounts, :max_pinned_statuses], Config.get([:instance, :max_pinned_statuses], 0))
+ |> put_in([:statuses, :characters_reserved_per_url], 0)
+ |> Map.merge(%{
+ urls: %{
+ streaming: Pleroma.Web.Endpoint.websocket_url(),
+ status: Config.get([:instance, :status_page])
+ },
+ vapid: %{
+ public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key)
+ }
+ })
+ end
+
+ defp pleroma_configuration(instance) do
+ %{
+ metadata: %{
+ account_activation_required: Keyword.get(instance, :account_activation_required),
+ features: features(),
+ federation: federation(),
+ fields_limits: fields_limits(),
+ post_formats: Config.get([:instance, :allowed_post_formats]),
+ birthday_required: Config.get([:instance, :birthday_required]),
+ birthday_min_age: Config.get([:instance, :birthday_min_age])
+ },
+ stats: %{mau: Pleroma.User.active_user_count()},
+ vapid_public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key)
+ }
+ end
+
+ defp pleroma_configuration2(instance) do
+ configuration = pleroma_configuration(instance)
+
+ configuration
+ |> Map.merge(%{
+ metadata:
+ configuration.metadata
+ |> Map.merge(%{
+ avatar_upload_limit: Keyword.get(instance, :avatar_upload_limit),
+ background_upload_limit: Keyword.get(instance, :background_upload_limit),
+ banner_upload_limit: Keyword.get(instance, :banner_upload_limit),
+ background_image:
+ Pleroma.Web.Endpoint.url() <> Keyword.get(instance, :background_image),
+ chat_limit: Keyword.get(instance, :chat_limit),
+ description_limit: Keyword.get(instance, :description_limit),
+ shout_limit: Config.get([:shout, :limit])
+ })
+ })
+ end
end
diff --git a/lib/pleroma/web/mastodon_api/views/poll_view.ex b/lib/pleroma/web/mastodon_api/views/poll_view.ex
@@ -21,7 +21,10 @@ defmodule Pleroma.Web.MastodonAPI.PollView do
votes_count: votes_count,
voters_count: voters_count(object),
options: options,
- emojis: Pleroma.Web.MastodonAPI.StatusView.build_emojis(object.data["emoji"])
+ emojis: Pleroma.Web.MastodonAPI.StatusView.build_emojis(object.data["emoji"]),
+ pleroma: %{
+ non_anonymous: object.data["nonAnonymous"] || false
+ }
}
if params[:for] do
diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex
@@ -21,6 +21,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.MediaProxy
alias Pleroma.Web.PleromaAPI.EmojiReactionController
+ alias Pleroma.Web.RichMedia.Card
import Pleroma.Web.ActivityPub.Visibility, only: [get_visibility: 1, visible_for_user?: 2]
@@ -29,9 +30,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
# pagination is restricted to 40 activities at a time
defp fetch_rich_media_for_activities(activities) do
Enum.each(activities, fn activity ->
- spawn(fn ->
- Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
- end)
+ spawn(fn -> Card.get_by_activity(activity) end)
end)
end
@@ -113,9 +112,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
# To do: check AdminAPIControllerTest on the reasons behind nil activities in the list
activities = Enum.filter(opts.activities, & &1)
- # Start fetching rich media before doing anything else, so that later calls to get the cards
- # only block for timeout in the worst case, as opposed to
- # length(activities_with_links) * timeout
+ # Start prefetching rich media before doing anything else
fetch_rich_media_for_activities(activities)
replied_to_activities = get_replied_to_activities(activities)
quoted_activities = get_quoted_activities(activities)
@@ -184,7 +181,14 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
favorited = opts[:for] && opts[:for].ap_id in (object.data["likes"] || [])
- bookmarked = Activity.get_bookmark(reblogged_parent_activity, opts[:for]) != nil
+ bookmark = Activity.get_bookmark(reblogged_parent_activity, opts[:for])
+
+ bookmark_folder =
+ if bookmark != nil do
+ bookmark.folder_id
+ else
+ nil
+ end
mentions =
activity.recipients
@@ -213,7 +217,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
favourites_count: 0,
reblogged: reblogged?(reblogged_parent_activity, opts[:for]),
favourited: present?(favorited),
- bookmarked: present?(bookmarked),
+ bookmarked: present?(bookmark),
muted: false,
pinned: pinned?,
sensitive: false,
@@ -227,7 +231,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
emojis: [],
pleroma: %{
local: activity.local,
- pinned_at: pinned_at
+ pinned_at: pinned_at,
+ bookmark_folder: bookmark_folder
}
}
end
@@ -264,7 +269,14 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
favorited = opts[:for] && opts[:for].ap_id in (object.data["likes"] || [])
- bookmarked = Activity.get_bookmark(activity, opts[:for]) != nil
+ bookmark = Activity.get_bookmark(activity, opts[:for])
+
+ bookmark_folder =
+ if bookmark != nil do
+ bookmark.folder_id
+ else
+ nil
+ end
client_posted_this_activity = opts[:for] && user.id == opts[:for].id
@@ -349,7 +361,11 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
summary = object.data["summary"] || ""
- card = render("card.json", Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity))
+ card =
+ case Card.get_by_activity(activity) do
+ %Card{} = result -> render("card.json", result)
+ _ -> nil
+ end
url =
if user.local do
@@ -418,7 +434,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
favourites_count: like_count,
reblogged: reblogged?(activity, opts[:for]),
favourited: present?(favorited),
- bookmarked: present?(bookmarked),
+ bookmarked: present?(bookmark),
muted: muted,
pinned: pinned?,
sensitive: sensitive,
@@ -448,7 +464,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
emoji_reactions: emoji_reactions,
parent_visible: visible_for_user?(reply_to, opts[:for]),
pinned_at: pinned_at,
- quotes_count: object.data["quotesCount"] || 0
+ quotes_count: object.data["quotesCount"] || 0,
+ bookmark_folder: bookmark_folder
}
}
end
@@ -551,15 +568,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
}
end
- def render("card.json", %{rich_media: rich_media, page_url: page_url}) do
- page_url_data = URI.parse(page_url)
-
- page_url_data =
- if is_binary(rich_media["url"]) do
- URI.merge(page_url_data, URI.parse(rich_media["url"]))
- else
- page_url_data
- end
+ def render("card.json", %Card{fields: rich_media}) do
+ page_url_data = URI.parse(rich_media["url"])
page_url = page_url_data |> to_string
@@ -573,6 +583,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
provider_url: page_url_data.scheme <> "://" <> page_url_data.host,
url: page_url,
image: image_url,
+ image_description: rich_media["image:alt"] || "",
title: rich_media["title"] || "",
description: rich_media["description"] || "",
pleroma: %{
@@ -796,8 +807,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
URI.merge(page_url_data, image_url_data) |> to_string
end
- defp build_image_url(_, _), do: nil
-
defp get_source_text(%{"content" => content} = _source) do
content
end
diff --git a/lib/pleroma/web/mastodon_api/websocket_handler.ex b/lib/pleroma/web/mastodon_api/websocket_handler.ex
@@ -11,28 +11,21 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
alias Pleroma.Web.Streamer
alias Pleroma.Web.StreamerView
- @behaviour :cowboy_websocket
+ @behaviour Phoenix.Socket.Transport
# Client ping period.
@tick :timer.seconds(30)
- # Cowboy timeout period.
- @timeout :timer.seconds(60)
- # Hibernate every X messages
- @hibernate_every 100
-
- def init(%{qs: qs} = req, state) do
- with params <- Enum.into(:cow_qs.parse_qs(qs), %{}),
- sec_websocket <- :cowboy_req.header("sec-websocket-protocol", req, nil),
- access_token <- Map.get(params, "access_token"),
- {:ok, user, oauth_token} <- authenticate_request(access_token, sec_websocket),
- {:ok, topic} <- Streamer.get_topic(params["stream"], user, oauth_token, params) do
- req =
- if sec_websocket do
- :cowboy_req.set_resp_header("sec-websocket-protocol", sec_websocket, req)
- else
- req
- end
+ @impl Phoenix.Socket.Transport
+ def child_spec(_opts), do: :ignore
+
+ # This only prepares the connection and is not in the process yet
+ @impl Phoenix.Socket.Transport
+ def connect(%{params: params} = transport_info) do
+ with access_token <- Map.get(params, "access_token"),
+ {:ok, user, oauth_token} <- authenticate_request(access_token),
+ {:ok, topic} <-
+ Streamer.get_topic(params["stream"], user, oauth_token, params) do
topics =
if topic do
[topic]
@@ -40,41 +33,40 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
[]
end
- {:cowboy_websocket, req,
- %{user: user, topics: topics, oauth_token: oauth_token, count: 0, timer: nil},
- %{idle_timeout: @timeout}}
+ state = %{
+ user: user,
+ topics: topics,
+ oauth_token: oauth_token,
+ count: 0,
+ timer: nil
+ }
+
+ {:ok, state}
else
{:error, :bad_topic} ->
- Logger.debug("#{__MODULE__} bad topic #{inspect(req)}")
- req = :cowboy_req.reply(404, req)
- {:ok, req, state}
+ Logger.debug("#{__MODULE__} bad topic #{inspect(transport_info)}")
+
+ {:error, :bad_topic}
{:error, :unauthorized} ->
- Logger.debug("#{__MODULE__} authentication error: #{inspect(req)}")
- req = :cowboy_req.reply(401, req)
- {:ok, req, state}
+ Logger.debug("#{__MODULE__} authentication error: #{inspect(transport_info)}")
+ {:error, :unauthorized}
end
end
- def websocket_init(state) do
- Logger.debug(
- "#{__MODULE__} accepted websocket connection for user #{(state.user || %{id: "anonymous"}).id}, topics #{state.topics}"
- )
-
+ # All subscriptions/links and messages cannot be created
+ # until the processed is launched with init/1
+ @impl Phoenix.Socket.Transport
+ def init(state) do
Enum.each(state.topics, fn topic -> Streamer.add_socket(topic, state.oauth_token) end)
- {:ok, %{state | timer: timer()}}
- end
- # Client's Pong frame.
- def websocket_handle(:pong, state) do
- if state.timer, do: Process.cancel_timer(state.timer)
- {:ok, %{state | timer: timer()}}
- end
+ Process.send_after(self(), :ping, @tick)
- # We only receive pings for now
- def websocket_handle(:ping, state), do: {:ok, state}
+ {:ok, state}
+ end
- def websocket_handle({:text, text}, state) do
+ @impl Phoenix.Socket.Transport
+ def handle_in({text, [opcode: :text]}, state) do
with {:ok, %{} = event} <- Jason.decode(text) do
handle_client_event(event, state)
else
@@ -84,50 +76,47 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
end
end
- def websocket_handle(frame, state) do
+ def handle_in(frame, state) do
Logger.error("#{__MODULE__} received frame: #{inspect(frame)}")
{:ok, state}
end
- def websocket_info({:render_with_user, view, template, item, topic}, state) do
+ @impl Phoenix.Socket.Transport
+ def handle_info({:render_with_user, view, template, item, topic}, state) do
user = %User{} = User.get_cached_by_ap_id(state.user.ap_id)
unless Streamer.filtered_by_user?(user, item) do
- websocket_info({:text, view.render(template, item, user, topic)}, %{state | user: user})
+ message = view.render(template, item, user, topic)
+ {:push, {:text, message}, %{state | user: user}}
else
{:ok, state}
end
end
- def websocket_info({:text, message}, state) do
- # If the websocket processed X messages, force an hibernate/GC.
- # We don't hibernate at every message to balance CPU usage/latency with RAM usage.
- if state.count > @hibernate_every do
- {:reply, {:text, message}, %{state | count: 0}, :hibernate}
- else
- {:reply, {:text, message}, %{state | count: state.count + 1}}
- end
+ def handle_info({:text, text}, state) do
+ {:push, {:text, text}, state}
end
- # Ping tick. We don't re-queue a timer there, it is instead queued when :pong is received.
- # As we hibernate there, reset the count to 0.
- # If the client misses :pong, Cowboy will automatically timeout the connection after
- # `@idle_timeout`.
- def websocket_info(:tick, state) do
- {:reply, :ping, %{state | timer: nil, count: 0}, :hibernate}
+ def handle_info(:ping, state) do
+ Process.send_after(self(), :ping, @tick)
+
+ {:push, {:ping, ""}, state}
end
- def websocket_info(:close, state) do
- {:stop, state}
+ def handle_info(:close, state) do
+ {:stop, {:closed, 'connection closed by server'}, state}
end
- # State can be `[]` only in case we terminate before switching to websocket,
- # we already log errors for these cases in `init/1`, so just do nothing here
- def terminate(_reason, _req, []), do: :ok
+ def handle_info(msg, state) do
+ Logger.debug("#{__MODULE__} received info: #{inspect(msg)}")
- def terminate(reason, _req, state) do
+ {:ok, state}
+ end
+
+ @impl Phoenix.Socket.Transport
+ def terminate(reason, state) do
Logger.debug(
- "#{__MODULE__} terminating websocket connection for user #{(state.user || %{id: "anonymous"}).id}, topics #{state.topics || "?"}: #{inspect(reason)}"
+ "#{__MODULE__} terminating websocket connection for user #{(state.user || %{id: "anonymous"}).id}, topics #{state.topics || "?"}: #{inspect(reason)})"
)
Enum.each(state.topics, fn topic -> Streamer.remove_socket(topic) end)
@@ -135,16 +124,13 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
end
# Public streams without authentication.
- defp authenticate_request(nil, nil) do
+ defp authenticate_request(nil) do
{:ok, nil, nil}
end
# Authenticated streams.
- defp authenticate_request(access_token, sec_websocket) do
- token = access_token || sec_websocket
-
- with true <- is_bitstring(token),
- oauth_token = %Token{user_id: user_id} <- Repo.get_by(Token, token: token),
+ defp authenticate_request(access_token) do
+ with oauth_token = %Token{user_id: user_id} <- Repo.get_by(Token, token: access_token),
user = %User{} <- User.get_cached_by_id(user_id) do
{:ok, user, oauth_token}
else
@@ -152,36 +138,32 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
end
end
- defp timer do
- Process.send_after(self(), :tick, @tick)
- end
-
defp handle_client_event(%{"type" => "subscribe", "stream" => _topic} = params, state) do
with {_, {:ok, topic}} <-
{:topic, Streamer.get_topic(params["stream"], state.user, state.oauth_token, params)},
{_, false} <- {:subscribed, topic in state.topics} do
Streamer.add_socket(topic, state.oauth_token)
- {[
- {:text,
- StreamerView.render("pleroma_respond.json", %{type: "subscribe", result: "success"})}
- ], %{state | topics: [topic | state.topics]}}
+ message =
+ StreamerView.render("pleroma_respond.json", %{type: "subscribe", result: "success"})
+
+ {:reply, :ok, {:text, message}, %{state | topics: [topic | state.topics]}}
else
{:subscribed, true} ->
- {[
- {:text,
- StreamerView.render("pleroma_respond.json", %{type: "subscribe", result: "ignored"})}
- ], state}
+ message =
+ StreamerView.render("pleroma_respond.json", %{type: "subscribe", result: "ignored"})
+
+ {:reply, :error, {:text, message}, state}
{:topic, {:error, error}} ->
- {[
- {:text,
- StreamerView.render("pleroma_respond.json", %{
- type: "subscribe",
- result: "error",
- error: error
- })}
- ], state}
+ message =
+ StreamerView.render("pleroma_respond.json", %{
+ type: "subscribe",
+ result: "error",
+ error: error
+ })
+
+ {:reply, :error, {:text, message}, state}
end
end
@@ -191,26 +173,26 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
{_, true} <- {:subscribed, topic in state.topics} do
Streamer.remove_socket(topic)
- {[
- {:text,
- StreamerView.render("pleroma_respond.json", %{type: "unsubscribe", result: "success"})}
- ], %{state | topics: List.delete(state.topics, topic)}}
+ message =
+ StreamerView.render("pleroma_respond.json", %{type: "unsubscribe", result: "success"})
+
+ {:reply, :ok, {:text, message}, %{state | topics: List.delete(state.topics, topic)}}
else
{:subscribed, false} ->
- {[
- {:text,
- StreamerView.render("pleroma_respond.json", %{type: "unsubscribe", result: "ignored"})}
- ], state}
+ message =
+ StreamerView.render("pleroma_respond.json", %{type: "unsubscribe", result: "ignored"})
+
+ {:reply, :error, {:text, message}, state}
{:topic, {:error, error}} ->
- {[
- {:text,
- StreamerView.render("pleroma_respond.json", %{
- type: "unsubscribe",
- result: "error",
- error: error
- })}
- ], state}
+ message =
+ StreamerView.render("pleroma_respond.json", %{
+ type: "unsubscribe",
+ result: "error",
+ error: error
+ })
+
+ {:reply, :error, {:text, message}, state}
end
end
@@ -219,39 +201,47 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
state
) do
with {:auth, nil, nil} <- {:auth, state.user, state.oauth_token},
- {:ok, user, oauth_token} <- authenticate_request(access_token, nil) do
- {[
- {:text,
- StreamerView.render("pleroma_respond.json", %{
- type: "pleroma:authenticate",
- result: "success"
- })}
- ], %{state | user: user, oauth_token: oauth_token}}
+ {:ok, user, oauth_token} <- authenticate_request(access_token) do
+ message =
+ StreamerView.render("pleroma_respond.json", %{
+ type: "pleroma:authenticate",
+ result: "success"
+ })
+
+ {:reply, :ok, {:text, message}, %{state | user: user, oauth_token: oauth_token}}
else
{:auth, _, _} ->
- {[
- {:text,
- StreamerView.render("pleroma_respond.json", %{
- type: "pleroma:authenticate",
- result: "error",
- error: :already_authenticated
- })}
- ], state}
+ message =
+ StreamerView.render("pleroma_respond.json", %{
+ type: "pleroma:authenticate",
+ result: "error",
+ error: :already_authenticated
+ })
+
+ {:reply, :error, {:text, message}, state}
_ ->
- {[
- {:text,
- StreamerView.render("pleroma_respond.json", %{
- type: "pleroma:authenticate",
- result: "error",
- error: :unauthorized
- })}
- ], state}
+ message =
+ StreamerView.render("pleroma_respond.json", %{
+ type: "pleroma:authenticate",
+ result: "error",
+ error: :unauthorized
+ })
+
+ {:reply, :error, {:text, message}, state}
end
end
defp handle_client_event(params, state) do
Logger.error("#{__MODULE__} received unknown event: #{inspect(params)}")
- {[], state}
+ {:ok, state}
+ end
+
+ def handle_error(conn, :unauthorized) do
+ Plug.Conn.send_resp(conn, 401, "Unauthorized")
+ end
+
+ def handle_error(conn, _reason) do
+ Plug.Conn.send_resp(conn, 404, "Not Found")
end
end
diff --git a/lib/pleroma/web/media_proxy/media_proxy_controller.ex b/lib/pleroma/web/media_proxy/media_proxy_controller.ex
@@ -56,7 +56,7 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyController do
media_proxy_url = MediaProxy.url(url)
with {:ok, %{status: status} = head_response} when status in 200..299 <-
- Pleroma.HTTP.request("HEAD", media_proxy_url, [], [], pool: :media) do
+ Pleroma.HTTP.request(:head, media_proxy_url, "", [], pool: :media) do
content_type = Tesla.get_header(head_response, "content-type")
content_length = Tesla.get_header(head_response, "content-length")
content_length = content_length && String.to_integer(content_length)
diff --git a/lib/pleroma/web/nodeinfo/nodeinfo.ex b/lib/pleroma/web/nodeinfo/nodeinfo.ex
@@ -6,7 +6,7 @@ defmodule Pleroma.Web.Nodeinfo.Nodeinfo do
alias Pleroma.Config
alias Pleroma.Stats
alias Pleroma.User
- alias Pleroma.Web.Federator.Publisher
+ alias Pleroma.Web.ActivityPub.Publisher
alias Pleroma.Web.MastodonAPI.InstanceView
# returns a nodeinfo 2.0 map, since 2.1 just adds a repository field
diff --git a/lib/pleroma/web/o_auth/authorization.ex b/lib/pleroma/web/o_auth/authorization.ex
@@ -28,7 +28,7 @@ defmodule Pleroma.Web.OAuth.Authorization do
end
@spec create_authorization(App.t(), User.t() | %{}, [String.t()] | nil) ::
- {:ok, Authorization.t()} | {:error, Changeset.t()}
+ {:ok, Authorization.t()} | {:error, Ecto.Changeset.t()}
def create_authorization(%App{} = app, %User{} = user, scopes \\ nil) do
%{
scopes: scopes || app.scopes,
@@ -39,7 +39,7 @@ defmodule Pleroma.Web.OAuth.Authorization do
|> Repo.insert()
end
- @spec create_changeset(map()) :: Changeset.t()
+ @spec create_changeset(map()) :: Ecto.Changeset.t()
def create_changeset(attrs \\ %{}) do
%Authorization{}
|> cast(attrs, [:user_id, :app_id, :scopes, :valid_until])
@@ -58,7 +58,7 @@ defmodule Pleroma.Web.OAuth.Authorization do
put_change(changeset, :valid_until, NaiveDateTime.add(NaiveDateTime.utc_now(), lifespan))
end
- @spec use_changeset(Authtorizatiton.t(), map()) :: Changeset.t()
+ @spec use_changeset(Authorization.t(), map()) :: Ecto.Changeset.t()
def use_changeset(%Authorization{} = auth, params) do
auth
|> cast(params, [:used])
@@ -66,7 +66,7 @@ defmodule Pleroma.Web.OAuth.Authorization do
end
@spec use_token(Authorization.t()) ::
- {:ok, Authorization.t()} | {:error, Changeset.t()} | {:error, String.t()}
+ {:ok, Authorization.t()} | {:error, Ecto.Changeset.t()} | {:error, String.t()}
def use_token(%Authorization{used: false, valid_until: valid_until} = auth) do
if NaiveDateTime.diff(NaiveDateTime.utc_now(), valid_until) < 0 do
Repo.update(use_changeset(auth, %{used: true}))
diff --git a/lib/pleroma/web/o_auth/o_auth_controller.ex b/lib/pleroma/web/o_auth/o_auth_controller.ex
@@ -310,7 +310,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do
after_token_exchange(conn, %{token: token})
else
_error ->
- handle_token_exchange_error(conn, :invalid_credentails)
+ handle_token_exchange_error(conn, :invalid_credentials)
end
end
@@ -610,13 +610,8 @@ defmodule Pleroma.Web.OAuth.OAuthController do
end
end
- @spec validate_scopes(App.t(), map() | list()) ::
+ @spec validate_scopes(App.t(), list()) ::
{:ok, list()} | {:error, :missing_scopes | :unsupported_scopes}
- defp validate_scopes(%App{} = app, params) when is_map(params) do
- requested_scopes = Scopes.fetch_scopes(params, app.scopes)
- validate_scopes(app, requested_scopes)
- end
-
defp validate_scopes(%App{} = app, requested_scopes) when is_list(requested_scopes) do
Scopes.validate(requested_scopes, app.scopes)
end
diff --git a/lib/pleroma/web/o_auth/token.ex b/lib/pleroma/web/o_auth/token.ex
@@ -56,7 +56,8 @@ defmodule Pleroma.Web.OAuth.Token do
|> Repo.find_resource()
end
- @spec exchange_token(App.t(), Authorization.t()) :: {:ok, Token.t()} | {:error, Changeset.t()}
+ @spec exchange_token(App.t(), Authorization.t()) ::
+ {:ok, Token.t()} | {:error, Ecto.Changeset.t()}
def exchange_token(app, auth) do
with {:ok, auth} <- Authorization.use_token(auth),
true <- auth.app_id == app.id do
@@ -95,7 +96,7 @@ defmodule Pleroma.Web.OAuth.Token do
|> validate_required([:valid_until])
end
- @spec create(App.t(), User.t(), map()) :: {:ok, Token} | {:error, Changeset.t()}
+ @spec create(App.t(), User.t(), map()) :: {:ok, Token} | {:error, Ecto.Changeset.t()}
def create(%App{} = app, %User{} = user, attrs \\ %{}) do
with {:ok, token} <- do_create(app, user, attrs) do
if Pleroma.Config.get([:oauth2, :clean_expired_tokens]) do
@@ -137,9 +138,9 @@ defmodule Pleroma.Web.OAuth.Token do
|> Repo.all()
end
- def is_expired?(%__MODULE__{valid_until: valid_until}) do
+ def expired?(%__MODULE__{valid_until: valid_until}) do
NaiveDateTime.diff(NaiveDateTime.utc_now(), valid_until) > 0
end
- def is_expired?(_), do: false
+ def expired?(_), do: false
end
diff --git a/lib/pleroma/web/o_auth/token/query.ex b/lib/pleroma/web/o_auth/token/query.ex
@@ -9,10 +9,10 @@ defmodule Pleroma.Web.OAuth.Token.Query do
import Ecto.Query, only: [from: 2]
- @type query :: Ecto.Queryable.t() | Token.t()
-
alias Pleroma.Web.OAuth.Token
+ @type query :: Ecto.Queryable.t() | Token.t()
+
@spec get_by_refresh_token(query, String.t()) :: query
def get_by_refresh_token(query \\ Token, refresh_token) do
from(q in query, where: q.refresh_token == ^refresh_token)
diff --git a/lib/pleroma/web/o_status/o_status_controller.ex b/lib/pleroma/web/o_status/o_status_controller.ex
@@ -37,7 +37,7 @@ defmodule Pleroma.Web.OStatus.OStatusController do
with id <- Endpoint.url() <> conn.request_path,
{_, %Activity{} = activity} <-
{:activity, Activity.get_create_by_object_ap_id_with_object(id)},
- {_, true} <- {:public?, Visibility.is_public?(activity)} do
+ {_, true} <- {:public?, Visibility.public?(activity)} do
redirect(conn, to: "/notice/#{activity.id}")
else
reason when reason in [{:public?, false}, {:activity, nil}] ->
@@ -56,7 +56,7 @@ defmodule Pleroma.Web.OStatus.OStatusController do
def activity(conn, _params) do
with id <- Endpoint.url() <> conn.request_path,
{_, %Activity{} = activity} <- {:activity, Activity.normalize(id)},
- {_, true} <- {:public?, Visibility.is_public?(activity)} do
+ {_, true} <- {:public?, Visibility.public?(activity)} do
redirect(conn, to: "/notice/#{activity.id}")
else
reason when reason in [{:public?, false}, {:activity, nil}] ->
@@ -69,7 +69,7 @@ defmodule Pleroma.Web.OStatus.OStatusController do
def notice(%{assigns: %{format: format}} = conn, %{"id" => id}) do
with {_, %Activity{} = activity} <- {:activity, Activity.get_by_id_with_object(id)},
- {_, true} <- {:public?, Visibility.is_public?(activity)},
+ {_, true} <- {:public?, Visibility.public?(activity)},
%User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
cond do
format in ["json", "activity+json"] ->
@@ -106,13 +106,12 @@ defmodule Pleroma.Web.OStatus.OStatusController do
# Returns an HTML embedded <audio> or <video> player suitable for embed iframes.
def notice_player(conn, %{"id" => id}) do
with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id_with_object(id),
- true <- Visibility.is_public?(activity),
+ true <- Visibility.public?(activity),
{_, true} <- {:visible?, Visibility.visible_for_user?(activity, _reading_user = nil)},
%Object{} = object <- Object.normalize(activity, fetch: false),
%{data: %{"attachment" => [%{"url" => [url | _]} | _]}} <- object,
true <- String.starts_with?(url["mediaType"], ["audio", "video"]) do
conn
- |> put_layout(:metadata_player)
|> put_resp_header("x-frame-options", "ALLOW")
|> put_resp_header(
"content-security-policy",
diff --git a/lib/pleroma/web/pleroma_api/controllers/bookmark_folder_controller.ex b/lib/pleroma/web/pleroma_api/controllers/bookmark_folder_controller.ex
@@ -0,0 +1,68 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.BookmarkFolderController do
+ use Pleroma.Web, :controller
+
+ alias Pleroma.BookmarkFolder
+ alias Pleroma.Web.Plugs.OAuthScopesPlug
+
+ plug(Pleroma.Web.ApiSpec.CastAndValidate)
+
+ # Note: scope not present in Mastodon: read:bookmarks
+ plug(OAuthScopesPlug, %{scopes: ["read:bookmarks"]} when action == :index)
+
+ # Note: scope not present in Mastodon: write:bookmarks
+ plug(
+ OAuthScopesPlug,
+ %{scopes: ["write:bookmarks"]} when action in [:create, :update, :delete]
+ )
+
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaBookmarkFolderOperation
+
+ action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+
+ def index(%{assigns: %{user: user}} = conn, _params) do
+ with folders <- BookmarkFolder.for_user(user.id) do
+ conn
+ |> render("index.json", %{folders: folders, as: :folder})
+ end
+ end
+
+ def create(
+ %{assigns: %{user: user}, private: %{open_api_spex: %{body_params: params}}} = conn,
+ _
+ ) do
+ with {:ok, folder} <- BookmarkFolder.create(user.id, params[:name], params[:emoji]) do
+ render(conn, "show.json", folder: folder)
+ end
+ end
+
+ def update(
+ %{
+ assigns: %{user: user},
+ private: %{open_api_spex: %{body_params: params, params: %{id: id}}}
+ } = conn,
+ _
+ ) do
+ with true <- BookmarkFolder.belongs_to_user?(id, user.id),
+ {:ok, folder} <- BookmarkFolder.update(id, params[:name], params[:emoji]) do
+ render(conn, "show.json", folder: folder)
+ else
+ false -> {:error, :forbidden}
+ end
+ end
+
+ def delete(
+ %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn,
+ _
+ ) do
+ with true <- BookmarkFolder.belongs_to_user?(id, user.id),
+ {:ok, folder} <- BookmarkFolder.delete(id) do
+ render(conn, "show.json", folder: folder)
+ else
+ false -> {:error, :forbidden}
+ end
+ end
+end
diff --git a/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex b/lib/pleroma/web/pleroma_api/controllers/chat_controller.ex
@@ -38,14 +38,24 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do
%{scopes: ["read:chats"]} when action in [:messages, :index, :index2, :show]
)
- plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.ChatOperation
- def delete_message(%{assigns: %{user: %{id: user_id} = user}} = conn, %{
- message_id: message_id,
- id: chat_id
- }) do
+ def delete_message(
+ %{
+ assigns: %{user: %{id: user_id} = user},
+ private: %{
+ open_api_spex: %{
+ params: %{
+ message_id: message_id,
+ id: chat_id
+ }
+ }
+ }
+ } = conn,
+ _
+ ) do
with %MessageReference{} = cm_ref <-
MessageReference.get_by_id(message_id),
^chat_id <- to_string(cm_ref.chat_id),
@@ -72,11 +82,14 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do
defp remove_or_delete(cm_ref, _), do: MessageReference.delete(cm_ref)
def post_chat_message(
- %{body_params: params, assigns: %{user: user}} = conn,
- %{id: id}
+ %{
+ private: %{open_api_spex: %{body_params: params, params: %{id: id}}},
+ assigns: %{user: user}
+ } = conn,
+ _
) do
with {:ok, chat} <- Chat.get_by_user_and_id(user, id),
- %User{} = recipient <- User.get_cached_by_ap_id(chat.recipient),
+ {_, %User{} = recipient} <- {:user, User.get_cached_by_ap_id(chat.recipient)},
{:ok, activity} <-
CommonAPI.post_chat_message(user, recipient, params[:content],
media_id: params[:media_id],
@@ -97,12 +110,20 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do
conn
|> put_status(:bad_request)
|> json(%{error: message})
+
+ {:user, nil} ->
+ conn
+ |> put_status(:bad_request)
+ |> json(%{error: "Recipient does not exist"})
end
end
def mark_message_as_read(
- %{assigns: %{user: %{id: user_id}}} = conn,
- %{id: chat_id, message_id: message_id}
+ %{
+ assigns: %{user: %{id: user_id}},
+ private: %{open_api_spex: %{params: %{id: chat_id, message_id: message_id}}}
+ } = conn,
+ _
) do
with %MessageReference{} = cm_ref <- MessageReference.get_by_id(message_id),
^chat_id <- to_string(cm_ref.chat_id),
@@ -115,8 +136,16 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do
end
def mark_as_read(
- %{body_params: %{last_read_id: last_read_id}, assigns: %{user: user}} = conn,
- %{id: id}
+ %{
+ assigns: %{user: user},
+ private: %{
+ open_api_spex: %{
+ body_params: %{last_read_id: last_read_id},
+ params: %{id: id}
+ }
+ }
+ } = conn,
+ _
) do
with {:ok, chat} <- Chat.get_by_user_and_id(user, id),
{_n, _} <- MessageReference.set_all_seen_for_chat(chat, last_read_id) do
@@ -124,7 +153,13 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do
end
end
- def messages(%{assigns: %{user: user}} = conn, %{id: id} = params) do
+ def messages(
+ %{
+ assigns: %{user: user},
+ private: %{open_api_spex: %{params: %{id: id} = params}}
+ } = conn,
+ _
+ ) do
with {:ok, chat} <- Chat.get_by_user_and_id(user, id) do
chat_message_refs =
chat
@@ -138,7 +173,7 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do
end
end
- def index(%{assigns: %{user: user}} = conn, params) do
+ def index(%{assigns: %{user: user}, private: %{open_api_spex: %{params: params}}} = conn, _) do
chats =
index_query(user, params)
|> Repo.all()
@@ -146,7 +181,7 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do
render(conn, "index.json", chats: chats)
end
- def index2(%{assigns: %{user: user}} = conn, params) do
+ def index2(%{assigns: %{user: user}, private: %{open_api_spex: %{params: params}}} = conn, _) do
chats =
index_query(user, params)
|> Pagination.fetch_paginated(params)
@@ -166,14 +201,14 @@ defmodule Pleroma.Web.PleromaAPI.ChatController do
|> where([c], c.recipient not in ^exclude_users)
end
- def create(%{assigns: %{user: user}} = conn, %{id: id}) do
+ def create(%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn, _) do
with %User{ap_id: recipient} <- User.get_cached_by_id(id),
{:ok, %Chat{} = chat} <- Chat.get_or_create(user.id, recipient) do
render(conn, "show.json", chat: chat)
end
end
- def show(%{assigns: %{user: user}} = conn, %{id: id}) do
+ def show(%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn, _) do
with {:ok, chat} <- Chat.get_by_user_and_id(user, id) do
render(conn, "show.json", chat: chat)
end
diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_file_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_file_controller.ex
@@ -8,7 +8,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiFileController do
alias Pleroma.Emoji.Pack
alias Pleroma.Web.ApiSpec
- plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
plug(
Pleroma.Web.Plugs.OAuthScopesPlug,
@@ -22,7 +22,10 @@ defmodule Pleroma.Web.PleromaAPI.EmojiFileController do
defdelegate open_api_operation(action), to: ApiSpec.PleromaEmojiFileOperation
- def create(%{body_params: params} = conn, %{name: pack_name}) do
+ def create(
+ %{private: %{open_api_spex: %{body_params: params, params: %{name: pack_name}}}} = conn,
+ _
+ ) do
filename = params[:filename] || get_filename(params[:file])
shortcode = params[:shortcode] || Path.basename(filename, Path.extname(filename))
@@ -49,7 +52,17 @@ defmodule Pleroma.Web.PleromaAPI.EmojiFileController do
end
end
- def update(%{body_params: %{shortcode: shortcode} = params} = conn, %{name: pack_name}) do
+ def update(
+ %{
+ private: %{
+ open_api_spex: %{
+ body_params: %{shortcode: shortcode} = params,
+ params: %{name: pack_name}
+ }
+ }
+ } = conn,
+ _
+ ) do
new_shortcode = params[:new_shortcode]
new_filename = params[:new_filename]
force = params[:force]
@@ -80,7 +93,10 @@ defmodule Pleroma.Web.PleromaAPI.EmojiFileController do
end
end
- def delete(conn, %{name: pack_name, shortcode: shortcode}) do
+ def delete(
+ %{private: %{open_api_spex: %{params: %{name: pack_name, shortcode: shortcode}}}} = conn,
+ _
+ ) do
with {:ok, pack} <- Pack.load_pack(pack_name),
{:ok, pack} <- Pack.delete_file(pack, shortcode) do
json(conn, pack.files)
diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex
@@ -7,7 +7,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackController do
alias Pleroma.Emoji.Pack
- plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
plug(
Pleroma.Web.Plugs.OAuthScopesPlug,
@@ -26,7 +26,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackController do
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaEmojiPackOperation
- def remote(conn, params) do
+ def remote(%{private: %{open_api_spex: %{params: params}}} = conn, _) do
with {:ok, packs} <-
Pack.list_remote(url: params.url, page_size: params.page_size, page: params.page) do
json(conn, packs)
@@ -38,7 +38,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackController do
end
end
- def index(conn, params) do
+ def index(%{private: %{open_api_spex: %{params: params}}} = conn, _) do
emoji_path =
[:instance, :static_dir]
|> Pleroma.Config.get!()
@@ -61,7 +61,11 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackController do
end
end
- def show(conn, %{name: name, page: page, page_size: page_size}) do
+ def show(
+ %{private: %{open_api_spex: %{params: %{name: name, page: page, page_size: page_size}}}} =
+ conn,
+ _
+ ) do
name = String.trim(name)
with {:ok, pack} <- Pack.show(name: name, page: page, page_size: page_size) do
@@ -90,7 +94,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackController do
end
end
- def archive(conn, %{name: name}) do
+ def archive(%{private: %{open_api_spex: %{params: %{name: name}}}} = conn, _) do
with {:ok, archive} <- Pack.get_archive(name) do
send_download(conn, {:binary, archive}, filename: "#{name}.zip")
else
@@ -109,7 +113,10 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackController do
end
end
- def download(%{body_params: %{url: url, name: name} = params} = conn, _) do
+ def download(
+ %{private: %{open_api_spex: %{body_params: %{url: url, name: name} = params}}} = conn,
+ _
+ ) do
with {:ok, _pack} <- Pack.download(name, url, params[:as]) do
json(conn, "ok")
else
@@ -130,7 +137,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackController do
end
end
- def create(conn, %{name: name}) do
+ def create(%{private: %{open_api_spex: %{params: %{name: name}}}} = conn, _) do
name = String.trim(name)
with {:ok, _pack} <- Pack.create(name) do
@@ -159,7 +166,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackController do
end
end
- def delete(conn, %{name: name}) do
+ def delete(%{private: %{open_api_spex: %{params: %{name: name}}}} = conn, _) do
name = String.trim(name)
with {:ok, deleted} when deleted != [] <- Pack.delete(name) do
@@ -184,7 +191,11 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackController do
end
end
- def update(%{body_params: %{metadata: metadata}} = conn, %{name: name}) do
+ def update(
+ %{private: %{open_api_spex: %{body_params: %{metadata: metadata}, params: %{name: name}}}} =
+ conn,
+ _
+ ) do
with {:ok, pack} <- Pack.update_metadata(name, metadata) do
json(conn, pack.pack)
else
diff --git a/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex b/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex
@@ -10,7 +10,7 @@ defmodule Pleroma.Web.PleromaAPI.MascotController do
alias Pleroma.Web.Plugs.OAuthScopesPlug
plug(Majic.Plug, [pool: Pleroma.MajicPool] when action in [:update])
- plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
plug(OAuthScopesPlug, %{scopes: ["read:accounts"]} when action == :show)
plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action != :show)
@@ -22,9 +22,13 @@ defmodule Pleroma.Web.PleromaAPI.MascotController do
end
@doc "PUT /api/v1/pleroma/mascot"
- def update(%{assigns: %{user: user}, body_params: %{file: file}} = conn, _) do
+ def update(
+ %{assigns: %{user: user}, private: %{open_api_spex: %{body_params: %{file: file}}}} =
+ conn,
+ _
+ ) do
with {:content_type, "image" <> _} <- {:content_type, file.content_type},
- {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)) do
+ {_, {:ok, object}} <- {:upload, ActivityPub.upload(file, actor: User.ap_id(user))} do
attachment = render_attachment(object)
{:ok, _user} = User.mascot_update(user, attachment)
@@ -32,6 +36,9 @@ defmodule Pleroma.Web.PleromaAPI.MascotController do
else
{:content_type, _} ->
render_error(conn, :unsupported_media_type, "mascots can only be images")
+
+ {:upload, {:error, _}} ->
+ render_error(conn, :error, "error uploading file")
end
end
diff --git a/lib/pleroma/web/pleroma_api/controllers/notification_controller.ex b/lib/pleroma/web/pleroma_api/controllers/notification_controller.ex
@@ -7,7 +7,7 @@ defmodule Pleroma.Web.PleromaAPI.NotificationController do
alias Pleroma.Notification
- plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
plug(
Pleroma.Web.Plugs.OAuthScopesPlug,
@@ -16,9 +16,16 @@ defmodule Pleroma.Web.PleromaAPI.NotificationController do
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaNotificationOperation
- def mark_as_read(%{assigns: %{user: user}, body_params: %{id: notification_id}} = conn, _) do
- with {:ok, notification} <- Notification.read_one(user, notification_id) do
- render(conn, "show.json", notification: notification, for: user)
+ def mark_as_read(
+ %{
+ assigns: %{user: user},
+ private: %{open_api_spex: %{body_params: %{id: notification_id}}}
+ } = conn,
+ _
+ ) do
+ with {:ok, _} <- Notification.read_one(user, notification_id) do
+ conn
+ |> json("ok")
else
{:error, message} ->
conn
@@ -27,12 +34,19 @@ defmodule Pleroma.Web.PleromaAPI.NotificationController do
end
end
- def mark_as_read(%{assigns: %{user: user}, body_params: %{max_id: max_id}} = conn, _) do
- notifications =
- user
- |> Notification.set_read_up_to(max_id)
- |> Enum.take(80)
-
- render(conn, "index.json", notifications: notifications, for: user)
+ def mark_as_read(
+ %{assigns: %{user: user}, private: %{open_api_spex: %{body_params: %{max_id: max_id}}}} =
+ conn,
+ _
+ ) do
+ with {:ok, _} <- Notification.set_read_up_to(user, max_id) do
+ conn
+ |> json("ok")
+ else
+ {:error, message} ->
+ conn
+ |> put_status(:bad_request)
+ |> json(%{"error" => message})
+ end
end
end
diff --git a/lib/pleroma/web/pleroma_api/controllers/user_import_controller.ex b/lib/pleroma/web/pleroma_api/controllers/user_import_controller.ex
@@ -15,14 +15,21 @@ defmodule Pleroma.Web.PleromaAPI.UserImportController do
plug(OAuthScopesPlug, %{scopes: ["follow", "write:blocks"]} when action == :blocks)
plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action == :mutes)
- plug(Pleroma.Web.ApiSpec.CastAndValidate)
+ plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
defdelegate open_api_operation(action), to: ApiSpec.UserImportOperation
- def follow(%{body_params: %{list: %Plug.Upload{path: path}}} = conn, _) do
- follow(%Plug.Conn{conn | body_params: %{list: File.read!(path)}}, %{})
+ def follow(
+ %{private: %{open_api_spex: %{body_params: %{list: %Plug.Upload{path: path}}}}} = conn,
+ _
+ ) do
+ list = File.read!(path)
+ do_follow(conn, list)
end
- def follow(%{assigns: %{user: follower}, body_params: %{list: list}} = conn, _) do
+ def follow(%{private: %{open_api_spex: %{body_params: %{list: list}}}} = conn, _),
+ do: do_follow(conn, list)
+
+ def do_follow(%{assigns: %{user: follower}} = conn, list) do
identifiers =
list
|> String.split("\n")
@@ -35,20 +42,34 @@ defmodule Pleroma.Web.PleromaAPI.UserImportController do
json(conn, "job started")
end
- def blocks(%{body_params: %{list: %Plug.Upload{path: path}}} = conn, _) do
- blocks(%Plug.Conn{conn | body_params: %{list: File.read!(path)}}, %{})
+ def blocks(
+ %{private: %{open_api_spex: %{body_params: %{list: %Plug.Upload{path: path}}}}} = conn,
+ _
+ ) do
+ list = File.read!(path)
+ do_block(conn, list)
end
- def blocks(%{assigns: %{user: blocker}, body_params: %{list: list}} = conn, _) do
+ def blocks(%{private: %{open_api_spex: %{body_params: %{list: list}}}} = conn, _),
+ do: do_block(conn, list)
+
+ defp do_block(%{assigns: %{user: blocker}} = conn, list) do
User.Import.blocks_import(blocker, prepare_user_identifiers(list))
json(conn, "job started")
end
- def mutes(%{body_params: %{list: %Plug.Upload{path: path}}} = conn, _) do
- mutes(%Plug.Conn{conn | body_params: %{list: File.read!(path)}}, %{})
+ def mutes(
+ %{private: %{open_api_spex: %{body_params: %{list: %Plug.Upload{path: path}}}}} = conn,
+ _
+ ) do
+ list = File.read!(path)
+ do_mute(conn, list)
end
- def mutes(%{assigns: %{user: user}, body_params: %{list: list}} = conn, _) do
+ def mutes(%{private: %{open_api_spex: %{body_params: %{list: list}}}} = conn, _),
+ do: do_mute(conn, list)
+
+ defp do_mute(%{assigns: %{user: user}} = conn, list) do
User.Import.mutes_import(user, prepare_user_identifiers(list))
json(conn, "job started")
end
diff --git a/lib/pleroma/web/pleroma_api/views/bookmark_folder_view.ex b/lib/pleroma/web/pleroma_api/views/bookmark_folder_view.ex
@@ -0,0 +1,42 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.BookmarkFolderView do
+ use Pleroma.Web, :view
+
+ alias Pleroma.BookmarkFolder
+ alias Pleroma.Emoji
+ alias Pleroma.Web.Endpoint
+
+ def render("show.json", %{folder: %BookmarkFolder{} = folder}) do
+ %{
+ id: folder.id |> to_string(),
+ name: folder.name,
+ emoji: folder.emoji,
+ emoji_url: get_emoji_url(folder.emoji)
+ }
+ end
+
+ def render("index.json", %{folders: folders} = opts) do
+ render_many(folders, __MODULE__, "show.json", Map.delete(opts, :folders))
+ end
+
+ defp get_emoji_url(nil) do
+ nil
+ end
+
+ defp get_emoji_url(emoji) do
+ if Emoji.unicode?(emoji) do
+ nil
+ else
+ emoji = Emoji.get(emoji)
+
+ if emoji != nil do
+ Endpoint.url() |> URI.merge(emoji.file) |> to_string()
+ else
+ nil
+ end
+ end
+ end
+end
diff --git a/lib/pleroma/web/pleroma_api/views/chat/message_reference_view.ex b/lib/pleroma/web/pleroma_api/views/chat/message_reference_view.ex
@@ -9,6 +9,7 @@ defmodule Pleroma.Web.PleromaAPI.Chat.MessageReferenceView do
alias Pleroma.User
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MastodonAPI.StatusView
+ alias Pleroma.Web.RichMedia.Card
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
@@ -23,6 +24,12 @@ defmodule Pleroma.Web.PleromaAPI.Chat.MessageReferenceView do
}
}
) do
+ card =
+ case Card.get_by_object(object) do
+ %Card{} = card_data -> StatusView.render("card.json", card_data)
+ _ -> nil
+ end
+
%{
id: id |> to_string(),
content: chat_message["content"],
@@ -34,11 +41,7 @@ defmodule Pleroma.Web.PleromaAPI.Chat.MessageReferenceView do
chat_message["attachment"] &&
StatusView.render("attachment.json", attachment: chat_message["attachment"]),
unread: unread,
- card:
- StatusView.render(
- "card.json",
- Pleroma.Web.RichMedia.Helpers.fetch_data_for_object(object)
- )
+ card: card
}
|> put_idempotency_key()
end
diff --git a/lib/pleroma/web/plugs/cache.ex b/lib/pleroma/web/plugs/cache.ex
@@ -20,7 +20,7 @@ defmodule Pleroma.Web.Plugs.Cache do
- `ttl`: An expiration time (time-to-live). This value should be in milliseconds or `nil` to disable expiration. Defaults to `nil`.
- `query_params`: Take URL query string into account (`true`), ignore it (`false`) or limit to specific params only (list). Defaults to `true`.
- - `tracking_fun`: A function that is called on successfull responses, no matter if the request is cached or not. It should accept a conn as the first argument and the value assigned to `tracking_fun_data` as the second.
+ - `tracking_fun`: A function that is called on successful responses, no matter if the request is cached or not. It should accept a conn as the first argument and the value assigned to `tracking_fun_data` as the second.
Additionally, you can overwrite the TTL inside a controller action by assigning `cache_ttl` to the connection struct:
diff --git a/lib/pleroma/web/plugs/o_auth_plug.ex b/lib/pleroma/web/plugs/o_auth_plug.ex
@@ -23,14 +23,14 @@ defmodule Pleroma.Web.Plugs.OAuthPlug do
def call(conn, _) do
with {:ok, token_str} <- fetch_token_str(conn) do
with {:ok, user, user_token} <- fetch_user_and_token(token_str),
- false <- Token.is_expired?(user_token) do
+ false <- Token.expired?(user_token) do
conn
|> assign(:token, user_token)
|> assign(:user, user)
else
_ ->
with {:ok, app, app_token} <- fetch_app_and_token(token_str),
- false <- Token.is_expired?(app_token) do
+ false <- Token.expired?(app_token) do
conn
|> assign(:token, app_token)
|> assign(:app, app)
diff --git a/lib/pleroma/web/plugs/rate_limiter/supervisor.ex b/lib/pleroma/web/plugs/rate_limiter/supervisor.ex
@@ -14,7 +14,7 @@ defmodule Pleroma.Web.Plugs.RateLimiter.Supervisor do
Pleroma.Web.Plugs.RateLimiter.LimiterSupervisor
]
- opts = [strategy: :one_for_one, name: Pleroma.Web.Streamer.Supervisor]
+ opts = [strategy: :one_for_one]
Supervisor.init(children, opts)
end
end
diff --git a/lib/pleroma/web/plugs/remote_ip.ex b/lib/pleroma/web/plugs/remote_ip.ex
@@ -43,6 +43,6 @@ defmodule Pleroma.Web.Plugs.RemoteIp do
InetCidr.v6?(InetCidr.parse_address!(proxy)) -> proxy <> "/128"
end
- InetCidr.parse(proxy, true)
+ InetCidr.parse_cidr!(proxy, true)
end
end
diff --git a/lib/pleroma/web/plugs/uploaded_media.ex b/lib/pleroma/web/plugs/uploaded_media.ex
@@ -105,7 +105,7 @@ defmodule Pleroma.Web.Plugs.UploadedMedia do
end
defp get_media(conn, unknown, _, _) do
- Logger.error("#{__MODULE__}: Unknown get startegy: #{inspect(unknown)}")
+ Logger.error("#{__MODULE__}: Unknown get strategy: #{inspect(unknown)}")
conn
|> send_resp(:internal_server_error, dgettext("errors", "Internal Error"))
diff --git a/lib/pleroma/web/rich_media/backfill.ex b/lib/pleroma/web/rich_media/backfill.ex
@@ -0,0 +1,101 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.RichMedia.Backfill do
+ alias Pleroma.Web.RichMedia.Card
+ alias Pleroma.Web.RichMedia.Parser
+ alias Pleroma.Web.RichMedia.Parser.TTL
+ alias Pleroma.Workers.RichMediaExpirationWorker
+
+ require Logger
+
+ @backfiller Pleroma.Config.get([__MODULE__, :provider], Pleroma.Web.RichMedia.Backfill.Task)
+ @cachex Pleroma.Config.get([:cachex, :provider], Cachex)
+ @max_attempts 3
+ @retry 5_000
+
+ def start(%{url: url} = args) when is_binary(url) do
+ url_hash = Card.url_to_hash(url)
+
+ args =
+ args
+ |> Map.put(:attempt, 1)
+ |> Map.put(:url_hash, url_hash)
+
+ @backfiller.run(args)
+ end
+
+ def run(%{url: url, url_hash: url_hash, attempt: attempt} = args)
+ when attempt <= @max_attempts do
+ case Parser.parse(url) do
+ {:ok, fields} ->
+ {:ok, card} = Card.create(url, fields)
+
+ maybe_schedule_expiration(url, fields)
+
+ if Map.has_key?(args, :activity_id) do
+ stream_update(args)
+ end
+
+ warm_cache(url_hash, card)
+
+ {:error, {:invalid_metadata, fields}} ->
+ Logger.debug("Rich media incomplete or invalid metadata for #{url}: #{inspect(fields)}")
+ negative_cache(url_hash)
+
+ {:error, :body_too_large} ->
+ Logger.error("Rich media error for #{url}: :body_too_large")
+ negative_cache(url_hash)
+
+ {:error, {:content_type, type}} ->
+ Logger.debug("Rich media error for #{url}: :content_type is #{type}")
+ negative_cache(url_hash)
+
+ e ->
+ Logger.debug("Rich media error for #{url}: #{inspect(e)}")
+
+ :timer.sleep(@retry * attempt)
+
+ run(%{args | attempt: attempt + 1})
+ end
+ end
+
+ def run(%{url: url, url_hash: url_hash}) do
+ Logger.debug("Rich media failure for #{url}")
+
+ negative_cache(url_hash, :timer.minutes(15))
+ end
+
+ defp maybe_schedule_expiration(url, fields) do
+ case TTL.process(fields, url) do
+ {:ok, ttl} when is_number(ttl) ->
+ timestamp = DateTime.from_unix!(ttl)
+
+ RichMediaExpirationWorker.new(%{"url" => url}, scheduled_at: timestamp)
+ |> Oban.insert()
+
+ _ ->
+ :ok
+ end
+ end
+
+ defp stream_update(%{activity_id: activity_id}) do
+ Pleroma.Activity.get_by_id(activity_id)
+ |> Pleroma.Activity.normalize()
+ |> Pleroma.Web.ActivityPub.ActivityPub.stream_out()
+ end
+
+ defp warm_cache(key, val), do: @cachex.put(:rich_media_cache, key, val)
+ defp negative_cache(key, ttl \\ nil), do: @cachex.put(:rich_media_cache, key, nil, ttl: ttl)
+end
+
+defmodule Pleroma.Web.RichMedia.Backfill.Task do
+ alias Pleroma.Web.RichMedia.Backfill
+
+ def run(args) do
+ Task.Supervisor.start_child(Pleroma.TaskSupervisor, Backfill, :run, [args],
+ name: {:global, {:rich_media, args.url_hash}}
+ )
+ end
+end
diff --git a/lib/pleroma/web/rich_media/card.ex b/lib/pleroma/web/rich_media/card.ex
@@ -0,0 +1,157 @@
+defmodule Pleroma.Web.RichMedia.Card do
+ use Ecto.Schema
+ import Ecto.Changeset
+ import Ecto.Query
+
+ alias Pleroma.Activity
+ alias Pleroma.HTML
+ alias Pleroma.Object
+ alias Pleroma.Repo
+ alias Pleroma.Web.RichMedia.Backfill
+ alias Pleroma.Web.RichMedia.Parser
+
+ @cachex Pleroma.Config.get([:cachex, :provider], Cachex)
+ @config_impl Application.compile_env(:pleroma, [__MODULE__, :config_impl], Pleroma.Config)
+
+ @type t :: %__MODULE__{}
+
+ schema "rich_media_card" do
+ field(:url_hash, :binary)
+ field(:fields, :map)
+
+ timestamps()
+ end
+
+ @doc false
+ def changeset(card, attrs) do
+ card
+ |> cast(attrs, [:url_hash, :fields])
+ |> validate_required([:url_hash, :fields])
+ |> unique_constraint(:url_hash)
+ end
+
+ @spec create(String.t(), map()) :: {:ok, t()}
+ def create(url, fields) do
+ url_hash = url_to_hash(url)
+
+ fields = Map.put_new(fields, "url", url)
+
+ %__MODULE__{}
+ |> changeset(%{url_hash: url_hash, fields: fields})
+ |> Repo.insert(on_conflict: {:replace, [:fields]}, conflict_target: :url_hash)
+ end
+
+ @spec delete(String.t()) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} | :ok
+ def delete(url) do
+ url_hash = url_to_hash(url)
+ @cachex.del(:rich_media_cache, url_hash)
+
+ case get_by_url(url) do
+ %__MODULE__{} = card -> Repo.delete(card)
+ nil -> :ok
+ end
+ end
+
+ @spec get_by_url(String.t() | nil) :: t() | nil | :error
+ def get_by_url(url) when is_binary(url) do
+ if @config_impl.get([:rich_media, :enabled]) do
+ url_hash = url_to_hash(url)
+
+ @cachex.fetch!(:rich_media_cache, url_hash, fn _ ->
+ result =
+ __MODULE__
+ |> where(url_hash: ^url_hash)
+ |> Repo.one()
+
+ case result do
+ %__MODULE__{} = card -> {:commit, card}
+ _ -> {:ignore, nil}
+ end
+ end)
+ else
+ :error
+ end
+ end
+
+ def get_by_url(nil), do: nil
+
+ @spec get_or_backfill_by_url(String.t(), map()) :: t() | nil
+ def get_or_backfill_by_url(url, backfill_opts \\ %{}) do
+ case get_by_url(url) do
+ %__MODULE__{} = card ->
+ card
+
+ nil ->
+ backfill_opts = Map.put(backfill_opts, :url, url)
+
+ Backfill.start(backfill_opts)
+
+ nil
+
+ :error ->
+ nil
+ end
+ end
+
+ @spec get_by_object(Object.t()) :: t() | nil | :error
+ def get_by_object(object) do
+ case HTML.extract_first_external_url_from_object(object) do
+ nil -> nil
+ url -> get_or_backfill_by_url(url)
+ end
+ end
+
+ @spec get_by_activity(Activity.t()) :: t() | nil | :error
+ # Fake/Draft activity
+ def get_by_activity(%Activity{id: "pleroma:fakeid"} = activity) do
+ with %Object{} = object <- Object.normalize(activity, fetch: false),
+ url when not is_nil(url) <- HTML.extract_first_external_url_from_object(object) do
+ case get_by_url(url) do
+ # Cache hit
+ %__MODULE__{} = card ->
+ card
+
+ # Cache miss, but fetch for rendering the Draft
+ _ ->
+ with {:ok, fields} <- Parser.parse(url),
+ {:ok, card} <- create(url, fields) do
+ card
+ else
+ _ -> nil
+ end
+ end
+ else
+ _ ->
+ nil
+ end
+ end
+
+ def get_by_activity(activity) do
+ with %Object{} = object <- Object.normalize(activity, fetch: false),
+ {_, nil} <- {:cached, get_cached_url(object, activity.id)} do
+ nil
+ else
+ {:cached, url} ->
+ get_or_backfill_by_url(url, %{activity_id: activity.id})
+
+ _ ->
+ :error
+ end
+ end
+
+ @spec url_to_hash(String.t()) :: String.t()
+ def url_to_hash(url) do
+ :crypto.hash(:sha256, url) |> Base.encode16(case: :lower)
+ end
+
+ defp get_cached_url(object, activity_id) do
+ key = "URL|#{activity_id}"
+
+ @cachex.fetch!(:scrubber_cache, key, fn _ ->
+ url = HTML.extract_first_external_url_from_object(object)
+ Activity.HTML.add_cache_key_for(activity_id, key)
+
+ {:commit, url}
+ end)
+ end
+end
diff --git a/lib/pleroma/web/rich_media/helpers.ex b/lib/pleroma/web/rich_media/helpers.ex
@@ -3,87 +3,13 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.RichMedia.Helpers do
- alias Pleroma.Activity
- alias Pleroma.HTML
- alias Pleroma.Object
- alias Pleroma.Web.RichMedia.Parser
-
- @config_impl Application.compile_env(:pleroma, [__MODULE__, :config_impl], Pleroma.Config)
-
- @options [
- pool: :media,
- max_body: 2_000_000,
- recv_timeout: 2_000
- ]
-
- @spec validate_page_url(URI.t() | binary()) :: :ok | :error
- defp validate_page_url(page_url) when is_binary(page_url) do
- validate_tld = @config_impl.get([Pleroma.Formatter, :validate_tld])
-
- page_url
- |> Linkify.Parser.url?(validate_tld: validate_tld)
- |> parse_uri(page_url)
- end
-
- defp validate_page_url(%URI{host: host, scheme: "https", authority: authority})
- when is_binary(authority) do
- cond do
- host in @config_impl.get([:rich_media, :ignore_hosts], []) ->
- :error
-
- get_tld(host) in @config_impl.get([:rich_media, :ignore_tld], []) ->
- :error
-
- true ->
- :ok
- end
- end
-
- defp validate_page_url(_), do: :error
-
- defp parse_uri(true, url) do
- url
- |> URI.parse()
- |> validate_page_url
- end
-
- defp parse_uri(_, _), do: :error
-
- defp get_tld(host) do
- host
- |> String.split(".")
- |> Enum.reverse()
- |> hd
- end
-
- def fetch_data_for_object(object) do
- with true <- @config_impl.get([:rich_media, :enabled]),
- {:ok, page_url} <-
- HTML.extract_first_external_url_from_object(object),
- :ok <- validate_page_url(page_url),
- {:ok, rich_media} <- Parser.parse(page_url) do
- %{page_url: page_url, rich_media: rich_media}
- else
- _ -> %{}
- end
- end
-
- def fetch_data_for_activity(%Activity{data: %{"type" => "Create"}} = activity) do
- with true <- @config_impl.get([:rich_media, :enabled]),
- %Object{} = object <- Object.normalize(activity, fetch: false) do
- fetch_data_for_object(object)
- else
- _ -> %{}
- end
- end
-
- def fetch_data_for_activity(_), do: %{}
+ alias Pleroma.Config
def rich_media_get(url) do
headers = [{"user-agent", Pleroma.Application.user_agent() <> "; Bot"}]
head_check =
- case Pleroma.HTTP.head(url, headers, @options) do
+ case Pleroma.HTTP.head(url, headers, http_options()) do
# If the HEAD request didn't reach the server for whatever reason,
# we assume the GET that comes right after won't either
{:error, _} = e ->
@@ -98,7 +24,7 @@ defmodule Pleroma.Web.RichMedia.Helpers do
:ok
end
- with :ok <- head_check, do: Pleroma.HTTP.get(url, headers, @options)
+ with :ok <- head_check, do: Pleroma.HTTP.get(url, headers, http_options())
end
defp check_content_type(headers) do
@@ -114,12 +40,13 @@ defmodule Pleroma.Web.RichMedia.Helpers do
end
end
- @max_body @options[:max_body]
defp check_content_length(headers) do
+ max_body = Keyword.get(http_options(), :max_body)
+
case List.keyfind(headers, "content-length", 0) do
{_, maybe_content_length} ->
case Integer.parse(maybe_content_length) do
- {content_length, ""} when content_length <= @max_body -> :ok
+ {content_length, ""} when content_length <= max_body -> :ok
{_, ""} -> {:error, :body_too_large}
_ -> :ok
end
@@ -128,4 +55,11 @@ defmodule Pleroma.Web.RichMedia.Helpers do
:ok
end
end
+
+ defp http_options do
+ [
+ pool: :media,
+ max_body: Config.get([:rich_media, :max_body], 5_000_000)
+ ]
+ end
end
diff --git a/lib/pleroma/web/rich_media/parser.ex b/lib/pleroma/web/rich_media/parser.ex
@@ -5,137 +5,28 @@
defmodule Pleroma.Web.RichMedia.Parser do
require Logger
- @cachex Pleroma.Config.get([:cachex, :provider], Cachex)
+ @config_impl Application.compile_env(:pleroma, [__MODULE__, :config_impl], Pleroma.Config)
defp parsers do
Pleroma.Config.get([:rich_media, :parsers])
end
- def parse(nil), do: {:error, "No URL provided"}
-
- if Pleroma.Config.get(:env) == :test do
- @spec parse(String.t()) :: {:ok, map()} | {:error, any()}
- def parse(url), do: parse_url(url)
- else
- @spec parse(String.t()) :: {:ok, map()} | {:error, any()}
- def parse(url) do
- with {:ok, data} <- get_cached_or_parse(url),
- {:ok, _} <- set_ttl_based_on_image(data, url) do
- {:ok, data}
- end
- end
-
- defp get_cached_or_parse(url) do
- case @cachex.fetch(:rich_media_cache, url, fn ->
- case parse_url(url) do
- {:ok, _} = res ->
- {:commit, res}
-
- {:error, reason} = e ->
- # Unfortunately we have to log errors here, instead of doing that
- # along with ttl setting at the bottom. Otherwise we can get log spam
- # if more than one process was waiting for the rich media card
- # while it was generated. Ideally we would set ttl here as well,
- # so we don't override it number_of_waiters_on_generation
- # times, but one, obviously, can't set ttl for not-yet-created entry
- # and Cachex doesn't support returning ttl from the fetch callback.
- log_error(url, reason)
- {:commit, e}
- end
- end) do
- {action, res} when action in [:commit, :ok] ->
- case res do
- {:ok, _data} = res ->
- res
-
- {:error, reason} = e ->
- if action == :commit, do: set_error_ttl(url, reason)
- e
- end
-
- {:error, e} ->
- {:error, {:cachex_error, e}}
- end
- end
-
- defp set_error_ttl(_url, :body_too_large), do: :ok
- defp set_error_ttl(_url, {:content_type, _}), do: :ok
-
- # The TTL is not set for the errors above, since they are unlikely to change
- # with time
-
- defp set_error_ttl(url, _reason) do
- ttl = Pleroma.Config.get([:rich_media, :failure_backoff], 60_000)
- @cachex.expire(:rich_media_cache, url, ttl)
- :ok
- end
-
- defp log_error(url, {:invalid_metadata, data}) do
- Logger.debug(fn -> "Incomplete or invalid metadata for #{url}: #{inspect(data)}" end)
- end
-
- defp log_error(url, reason) do
- Logger.warning(fn -> "Rich media error for #{url}: #{inspect(reason)}" end)
- end
- end
-
- @doc """
- Set the rich media cache based on the expiration time of image.
-
- Adopt behaviour `Pleroma.Web.RichMedia.Parser.TTL`
-
- ## Example
-
- defmodule MyModule do
- @behaviour Pleroma.Web.RichMedia.Parser.TTL
- def ttl(data, url) do
- image_url = Map.get(data, :image)
- # do some parsing in the url and get the ttl of the image
- # and return ttl is unix time
- parse_ttl_from_url(image_url)
- end
- end
+ def parse(nil), do: nil
- Define the module in the config
-
- config :pleroma, :rich_media,
- ttl_setters: [MyModule]
- """
- @spec set_ttl_based_on_image(map(), String.t()) ::
- {:ok, Integer.t() | :noop} | {:error, :no_key}
- def set_ttl_based_on_image(data, url) do
- case get_ttl_from_image(data, url) do
- {:ok, ttl} when is_number(ttl) ->
- ttl = ttl * 1000
-
- case @cachex.expire_at(:rich_media_cache, url, ttl) do
- {:ok, true} -> {:ok, ttl}
- {:ok, false} -> {:error, :no_key}
- end
-
- _ ->
- {:ok, :noop}
+ @spec parse(String.t()) :: {:ok, map()} | {:error, any()}
+ def parse(url) do
+ with :ok <- validate_page_url(url),
+ {:ok, data} <- parse_url(url) do
+ data = Map.put(data, "url", url)
+ {:ok, data}
end
end
- defp get_ttl_from_image(data, url) do
- [:rich_media, :ttl_setters]
- |> Pleroma.Config.get()
- |> Enum.reduce({:ok, nil}, fn
- module, {:ok, _ttl} ->
- module.ttl(data, url)
-
- _, error ->
- error
- end)
- end
-
- def parse_url(url) do
+ defp parse_url(url) do
with {:ok, %Tesla.Env{body: html}} <- Pleroma.Web.RichMedia.Helpers.rich_media_get(url),
{:ok, html} <- Floki.parse_document(html) do
html
|> maybe_parse()
- |> Map.put("url", url)
|> clean_parsed_data()
|> check_parsed_data()
end
@@ -166,4 +57,46 @@ defmodule Pleroma.Web.RichMedia.Parser do
end)
|> Map.new()
end
+
+ @spec validate_page_url(URI.t() | binary()) :: :ok | :error
+ defp validate_page_url(page_url) when is_binary(page_url) do
+ validate_tld = @config_impl.get([Pleroma.Formatter, :validate_tld])
+
+ page_url
+ |> Linkify.Parser.url?(validate_tld: validate_tld)
+ |> parse_uri(page_url)
+ end
+
+ defp validate_page_url(%URI{host: host, scheme: "https"}) do
+ cond do
+ Linkify.Parser.ip?(host) ->
+ :error
+
+ host in @config_impl.get([:rich_media, :ignore_hosts], []) ->
+ :error
+
+ get_tld(host) in @config_impl.get([:rich_media, :ignore_tld], []) ->
+ :error
+
+ true ->
+ :ok
+ end
+ end
+
+ defp validate_page_url(_), do: :error
+
+ defp parse_uri(true, url) do
+ url
+ |> URI.parse()
+ |> validate_page_url
+ end
+
+ defp parse_uri(_, _), do: :error
+
+ defp get_tld(host) do
+ host
+ |> String.split(".")
+ |> Enum.reverse()
+ |> hd
+ end
end
diff --git a/lib/pleroma/web/rich_media/parser/ttl.ex b/lib/pleroma/web/rich_media/parser/ttl.ex
@@ -3,5 +3,18 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.RichMedia.Parser.TTL do
- @callback ttl(Map.t(), String.t()) :: Integer.t() | nil
+ @callback ttl(map(), String.t()) :: integer() | nil
+
+ @spec process(map(), String.t()) :: {:ok, integer() | nil}
+ def process(data, url) do
+ [:rich_media, :ttl_setters]
+ |> Pleroma.Config.get()
+ |> Enum.reduce_while({:ok, nil}, fn
+ module, acc ->
+ case module.ttl(data, url) do
+ ttl when is_number(ttl) -> {:halt, {:ok, ttl}}
+ _ -> {:cont, acc}
+ end
+ end)
+ end
end
diff --git a/lib/pleroma/web/rich_media/parser/ttl/aws_signed_url.ex b/lib/pleroma/web/rich_media/parser/ttl/aws_signed_url.ex
@@ -7,25 +7,26 @@ defmodule Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl do
@impl true
def ttl(data, _url) do
- image = Map.get(data, :image)
+ image = Map.get(data, "image")
- if is_aws_signed_url(image) do
+ if aws_signed_url?(image) do
image
|> parse_query_params()
|> format_query_params()
|> get_expiration_timestamp()
else
- {:error, "Not aws signed url #{inspect(image)}"}
+ nil
end
end
- defp is_aws_signed_url(image) when is_binary(image) and image != "" do
+ defp aws_signed_url?(image) when is_binary(image) and image != "" do
%URI{host: host, query: query} = URI.parse(image)
- String.contains?(host, "amazonaws.com") and String.contains?(query, "X-Amz-Expires")
+ is_binary(host) and String.contains?(host, "amazonaws.com") and
+ String.contains?(query, "X-Amz-Expires")
end
- defp is_aws_signed_url(_), do: nil
+ defp aws_signed_url?(_), do: nil
defp parse_query_params(image) do
%URI{query: query} = URI.parse(image)
@@ -45,6 +46,6 @@ defmodule Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl do
|> Map.get("X-Amz-Date")
|> Timex.parse("{ISO:Basic:Z}")
- {:ok, Timex.to_unix(date) + String.to_integer(Map.get(params, "X-Amz-Expires"))}
+ Timex.to_unix(date) + String.to_integer(Map.get(params, "X-Amz-Expires"))
end
end
diff --git a/lib/pleroma/web/rich_media/parser/ttl/opengraph.ex b/lib/pleroma/web/rich_media/parser/ttl/opengraph.ex
@@ -0,0 +1,20 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.RichMedia.Parser.TTL.Opengraph do
+ @behaviour Pleroma.Web.RichMedia.Parser.TTL
+
+ @impl true
+ def ttl(%{"ttl" => ttl_string}, _url) when is_binary(ttl_string) do
+ try do
+ ttl = String.to_integer(ttl_string)
+ now = DateTime.utc_now() |> DateTime.to_unix()
+ now + ttl
+ rescue
+ _ -> nil
+ end
+ end
+
+ def ttl(_, _), do: nil
+end
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
@@ -182,7 +182,7 @@ defmodule Pleroma.Web.Router do
end
pipeline :well_known do
- plug(:accepts, ["json", "jrd+json", "xml", "xrd+xml"])
+ plug(:accepts, ["json", "jrd", "jrd+json", "xml", "xrd+xml"])
end
pipeline :config do
@@ -292,6 +292,11 @@ defmodule Pleroma.Web.Router do
post("/frontends/install", FrontendController, :install)
post("/backups", AdminAPIController, :create_backup)
+
+ get("/rules", RuleController, :index)
+ post("/rules", RuleController, :create)
+ patch("/rules/:id", RuleController, :update)
+ delete("/rules/:id", RuleController, :delete)
end
# AdminAPI: admins and mods (staff) can perform these actions (if privileged by role)
@@ -481,7 +486,7 @@ defmodule Pleroma.Web.Router do
post("/change_email", UtilController, :change_email)
post("/change_password", UtilController, :change_password)
post("/delete_account", UtilController, :delete_account)
- put("/notification_settings", UtilController, :update_notificaton_settings)
+ put("/notification_settings", UtilController, :update_notification_settings)
post("/disable_account", UtilController, :disable_account)
post("/move_account", UtilController, :move_account)
@@ -580,6 +585,11 @@ defmodule Pleroma.Web.Router do
get("/backups", BackupController, :index)
post("/backups", BackupController, :create)
+
+ get("/bookmark_folders", BookmarkFolderController, :index)
+ post("/bookmark_folders", BookmarkFolderController, :create)
+ patch("/bookmark_folders/:id", BookmarkFolderController, :update)
+ delete("/bookmark_folders/:id", BookmarkFolderController, :delete)
end
scope [] do
@@ -628,6 +638,7 @@ defmodule Pleroma.Web.Router do
patch("/accounts/update_credentials", AccountController, :update_credentials)
get("/accounts/relationships", AccountController, :relationships)
+ get("/accounts/familiar_followers", AccountController, :familiar_followers)
get("/accounts/:id/lists", AccountController, :lists)
get("/accounts/:id/identity_proofs", AccountController, :identity_proofs)
get("/endorsements", AccountController, :endorsements)
@@ -759,11 +770,11 @@ defmodule Pleroma.Web.Router do
get("/instance", InstanceController, :show)
get("/instance/peers", InstanceController, :peers)
+ get("/instance/rules", InstanceController, :rules)
get("/statuses", StatusController, :index)
get("/statuses/:id", StatusController, :show)
get("/statuses/:id/context", StatusController, :context)
- get("/statuses/:id/card", StatusController, :card)
get("/statuses/:id/favourited_by", StatusController, :favourited_by)
get("/statuses/:id/reblogged_by", StatusController, :reblogged_by)
get("/statuses/:id/history", StatusController, :show_history)
@@ -783,11 +794,14 @@ defmodule Pleroma.Web.Router do
scope "/api/v2", Pleroma.Web.MastodonAPI do
pipe_through(:api)
+
get("/search", SearchController, :search2)
post("/media", MediaController, :create2)
get("/suggestions", SuggestionController, :index2)
+
+ get("/instance", InstanceController, :show2)
end
scope "/api", Pleroma.Web do
diff --git a/lib/pleroma/web/static_fe/static_fe_controller.ex b/lib/pleroma/web/static_fe/static_fe_controller.ex
@@ -13,7 +13,6 @@ defmodule Pleroma.Web.StaticFE.StaticFEController do
alias Pleroma.Web.Metadata
alias Pleroma.Web.Router.Helpers
- plug(:put_layout, :static_fe)
plug(:assign_id)
@page_keys ["max_id", "min_id", "limit", "since_id", "order"]
@@ -22,7 +21,7 @@ defmodule Pleroma.Web.StaticFE.StaticFEController do
def show(%{assigns: %{notice_id: notice_id}} = conn, _params) do
with %Activity{local: true} = activity <-
Activity.get_by_id_with_object(notice_id),
- true <- Visibility.is_public?(activity.object),
+ true <- Visibility.public?(activity.object),
{_, true} <- {:visible?, Visibility.visible_for_user?(activity, _reading_user = nil)},
%User{} = user <- User.get_by_ap_id(activity.object.data["actor"]) do
url = Helpers.url(conn) <> conn.request_path
diff --git a/lib/pleroma/web/streamer.ex b/lib/pleroma/web/streamer.ex
@@ -20,7 +20,6 @@ defmodule Pleroma.Web.Streamer do
alias Pleroma.Web.Plugs.OAuthScopesPlug
alias Pleroma.Web.StreamerView
- @mix_env Mix.env()
@registry Pleroma.Web.StreamerRegistry
def registry, do: @registry
@@ -34,7 +33,7 @@ defmodule Pleroma.Web.Streamer do
stream :: String.t(),
User.t() | nil,
Token.t() | nil,
- Map.t() | nil
+ map() | nil
) ::
{:ok, topic :: String.t()} | {:error, :bad_topic} | {:error, :unauthorized}
def get_topic_and_add_socket(stream, user, oauth_token, params \\ %{}) do
@@ -60,7 +59,7 @@ defmodule Pleroma.Web.Streamer do
end
@doc "Expand and authorizes a stream"
- @spec get_topic(stream :: String.t() | nil, User.t() | nil, Token.t() | nil, Map.t()) ::
+ @spec get_topic(stream :: String.t() | nil, User.t() | nil, Token.t() | nil, map()) ::
{:ok, topic :: String.t() | nil} | {:error, :bad_topic}
def get_topic(stream, user, oauth_token, params \\ %{})
@@ -246,7 +245,7 @@ defmodule Pleroma.Web.Streamer do
defp do_stream("list", item) do
# filter the recipient list if the activity is not public, see #270.
recipient_lists =
- case Visibility.is_public?(item) do
+ case Visibility.public?(item) do
true ->
Pleroma.List.get_lists_from_activity(item)
@@ -396,25 +395,20 @@ defmodule Pleroma.Web.Streamer do
end
end
- # In test environement, only return true if the registry is started.
- # In benchmark environment, returns false.
- # In any other environment, always returns true.
- cond do
- @mix_env == :test ->
- def should_env_send? do
- case Process.whereis(@registry) do
- nil ->
- false
+ # In dev/prod the streamer registry is expected to be started, so return true
+ # In test it is possible to have the registry started for a test so it will check
+ # In benchmark it will never find the process alive and return false
+ def should_env_send? do
+ if Application.get_env(:pleroma, Pleroma.Application)[:streamer_registry] do
+ true
+ else
+ case Process.whereis(@registry) do
+ nil ->
+ false
- pid ->
- Process.alive?(pid)
- end
+ pid ->
+ Process.alive?(pid)
end
-
- @mix_env == :benchmark ->
- def should_env_send?, do: false
-
- true ->
- def should_env_send?, do: true
+ end
end
end
diff --git a/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex
@@ -13,7 +13,7 @@
<div class="account-header__avatar" style="background-image: url('<%= Pleroma.User.avatar_url(@user) %>')"></div>
<div class="account-header__meta">
<div class="account-header__display-name"><%= @user.name %></div>
- <div class="account-header__nickname">@<%= @user.nickname %>@<%= Pleroma.User.get_host(@user) %></div>
+ <div class="account-header__nickname">@<%= Pleroma.User.full_nickname(@user.nickname) %></div>
</div>
</div>
<% end %>
diff --git a/lib/pleroma/web/twitter_api/controllers/password_controller.ex b/lib/pleroma/web/twitter_api/controllers/password_controller.ex
@@ -4,7 +4,7 @@
defmodule Pleroma.Web.TwitterAPI.PasswordController do
@moduledoc """
- The module containts functions for reset password.
+ The module contains functions for password reset.
"""
use Pleroma.Web, :controller
diff --git a/lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex b/lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex
@@ -29,7 +29,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowController do
# GET /ostatus_subscribe
#
def follow(%{assigns: %{user: user}} = conn, %{"acct" => acct}) do
- case is_status?(acct) do
+ case status?(acct) do
true -> follow_status(conn, user, acct)
_ -> follow_account(conn, user, acct)
end
@@ -57,7 +57,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowController do
defp follow_template(%User{} = _user), do: "follow.html"
defp follow_template(_), do: "follow_login.html"
- defp is_status?(acct) do
+ defp status?(acct) do
case Fetcher.fetch_and_contain_remote_object_from_id(acct) do
{:ok, %{"type" => type}} when type in @status_types ->
true
diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex
@@ -18,7 +18,8 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
alias Pleroma.Web.WebFinger
plug(
- Pleroma.Web.ApiSpec.CastAndValidate
+ Pleroma.Web.ApiSpec.CastAndValidate,
+ [replace_params: false]
when action != :remote_subscribe and action != :show_subscribe_form
)
@@ -35,7 +36,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
:change_email,
:change_password,
:delete_account,
- :update_notificaton_settings,
+ :update_notification_settings,
:disable_account,
:move_account,
:add_alias,
@@ -150,7 +151,10 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
end
end
- def remote_interaction(%{body_params: %{ap_id: ap_id, profile: profile}} = conn, _params) do
+ def remote_interaction(
+ %{private: %{open_api_spex: %{body_params: %{ap_id: ap_id, profile: profile}}}} = conn,
+ _params
+ ) do
with {:ok, %{"subscribe_address" => template}} <- WebFinger.finger(profile) do
conn
|> json(%{url: String.replace(template, "{uri}", ap_id)})
@@ -181,13 +185,16 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
json(conn, emoji)
end
- def update_notificaton_settings(%{assigns: %{user: user}} = conn, params) do
+ def update_notification_settings(%{assigns: %{user: user}} = conn, params) do
with {:ok, _} <- User.update_notification_settings(user, params) do
json(conn, %{status: "success"})
end
end
- def change_password(%{assigns: %{user: user}, body_params: body_params} = conn, %{}) do
+ def change_password(
+ %{assigns: %{user: user}, private: %{open_api_spex: %{body_params: body_params}}} = conn,
+ _
+ ) do
case CommonAPI.Utils.confirm_current_password(user, body_params.password) do
{:ok, user} ->
with {:ok, _user} <-
@@ -210,7 +217,10 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
end
end
- def change_email(%{assigns: %{user: user}, body_params: body_params} = conn, %{}) do
+ def change_email(
+ %{assigns: %{user: user}, private: %{open_api_spex: %{body_params: body_params}}} = conn,
+ _
+ ) do
case CommonAPI.Utils.confirm_current_password(user, body_params.password) do
{:ok, user} ->
with {:ok, _user} <- User.change_email(user, body_params.email) do
@@ -229,7 +239,13 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
end
end
- def delete_account(%{assigns: %{user: user}, body_params: body_params} = conn, params) do
+ def delete_account(
+ %{
+ assigns: %{user: user},
+ private: %{open_api_spex: %{body_params: body_params, params: params}}
+ } = conn,
+ _
+ ) do
# This endpoint can accept a query param or JSON body for backwards-compatibility.
# Submitting a JSON body is recommended, so passwords don't end up in server logs.
password = body_params[:password] || params[:password] || ""
@@ -244,7 +260,10 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
end
end
- def disable_account(%{assigns: %{user: user}} = conn, params) do
+ def disable_account(
+ %{assigns: %{user: user}, private: %{open_api_spex: %{params: params}}} = conn,
+ _
+ ) do
case CommonAPI.Utils.confirm_current_password(user, params[:password]) do
{:ok, user} ->
User.set_activation_async(user, false)
@@ -255,7 +274,10 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
end
end
- def move_account(%{assigns: %{user: user}, body_params: body_params} = conn, %{}) do
+ def move_account(
+ %{assigns: %{user: user}, private: %{open_api_spex: %{body_params: body_params}}} = conn,
+ _
+ ) do
case CommonAPI.Utils.confirm_current_password(user, body_params.password) do
{:ok, user} ->
with {:ok, target_user} <- find_or_fetch_user_by_nickname(body_params.target_account),
@@ -276,7 +298,10 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
end
end
- def add_alias(%{assigns: %{user: user}, body_params: body_params} = conn, _) do
+ def add_alias(
+ %{assigns: %{user: user}, private: %{open_api_spex: %{body_params: body_params}}} = conn,
+ _
+ ) do
with {:ok, alias_user} <- find_user_by_nickname(body_params.alias),
{:ok, _user} <- user |> User.add_alias(alias_user) do
json(conn, %{status: "success"})
@@ -291,7 +316,10 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
end
end
- def delete_alias(%{assigns: %{user: user}, body_params: body_params} = conn, _) do
+ def delete_alias(
+ %{assigns: %{user: user}, private: %{open_api_spex: %{body_params: body_params}}} = conn,
+ _
+ ) do
with {:ok, alias_user} <- find_user_by_nickname(body_params.alias),
{:ok, _user} <- user |> User.delete_alias(alias_user) do
json(conn, %{status: "success"})
@@ -306,7 +334,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
end
end
- def list_aliases(%{assigns: %{user: user}} = conn, %{}) do
+ def list_aliases(%{assigns: %{user: user}} = conn, _) do
alias_nicks =
user
|> User.alias_users()
@@ -319,7 +347,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
user = User.get_cached_by_nickname(nickname)
if user == nil do
- {:not_found, nil}
+ {:error, :not_found}
else
{:ok, user}
end
diff --git a/lib/pleroma/web/web_finger.ex b/lib/pleroma/web/web_finger.ex
@@ -5,8 +5,8 @@
defmodule Pleroma.Web.WebFinger do
alias Pleroma.HTTP
alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.Publisher
alias Pleroma.Web.Endpoint
- alias Pleroma.Web.Federator.Publisher
alias Pleroma.Web.XML
alias Pleroma.XmlBuilder
require Jason
@@ -70,7 +70,7 @@ defmodule Pleroma.Web.WebFinger do
def represent_user(user, "JSON") do
%{
- "subject" => "acct:#{user.nickname}@#{domain()}",
+ "subject" => "acct:#{user.nickname}@#{host()}",
"aliases" => gather_aliases(user),
"links" => gather_links(user)
}
@@ -90,13 +90,13 @@ defmodule Pleroma.Web.WebFinger do
:XRD,
%{xmlns: "http://docs.oasis-open.org/ns/xri/xrd-1.0"},
[
- {:Subject, "acct:#{user.nickname}@#{domain()}"}
+ {:Subject, "acct:#{user.nickname}@#{host()}"}
] ++ aliases ++ links
}
|> XmlBuilder.to_doc()
end
- defp domain do
+ def host do
Pleroma.Config.get([__MODULE__, :domain]) || Pleroma.Web.Endpoint.host()
end
diff --git a/lib/pleroma/web/web_finger/web_finger_controller.ex b/lib/pleroma/web/web_finger/web_finger_controller.ex
@@ -30,7 +30,7 @@ defmodule Pleroma.Web.WebFinger.WebFingerController do
end
def webfinger(%{assigns: %{format: format}} = conn, %{"resource" => resource})
- when format in ["json", "jrd+json"] do
+ when format in ["jrd", "json", "jrd+json"] do
with {:ok, response} <- WebFinger.webfinger(resource, "JSON") do
json(conn, response)
else
diff --git a/lib/pleroma/workers/background_worker.ex b/lib/pleroma/workers/background_worker.ex
@@ -28,7 +28,7 @@ defmodule Pleroma.Workers.BackgroundWorker do
def perform(%Job{args: %{"op" => op, "user_id" => user_id, "identifiers" => identifiers}})
when op in ["blocks_import", "follow_import", "mutes_import"] do
user = User.get_cached_by_id(user_id)
- {:ok, User.Import.perform(String.to_atom(op), user, identifiers)}
+ {:ok, User.Import.perform(String.to_existing_atom(op), user, identifiers)}
end
def perform(%Job{
@@ -40,6 +40,11 @@ defmodule Pleroma.Workers.BackgroundWorker do
Pleroma.FollowingRelationship.move_following(origin, target)
end
+ def perform(%Job{args: %{"op" => "verify_fields_links", "user_id" => user_id}}) do
+ user = User.get_by_id(user_id)
+ User.perform(:verify_fields_links, user)
+ end
+
def perform(%Job{args: %{"op" => "delete_instance", "host" => host}}) do
Instance.perform(:delete_instance, host)
end
diff --git a/lib/pleroma/workers/publisher_worker.ex b/lib/pleroma/workers/publisher_worker.ex
@@ -18,9 +18,9 @@ defmodule Pleroma.Workers.PublisherWorker do
Federator.perform(:publish, activity)
end
- def perform(%Job{args: %{"op" => "publish_one", "module" => module_name, "params" => params}}) do
+ def perform(%Job{args: %{"op" => "publish_one", "params" => params}}) do
params = Map.new(params, fn {k, v} -> {String.to_atom(k), v} end)
- Federator.perform(:publish_one, String.to_atom(module_name), params)
+ Federator.perform(:publish_one, params)
end
@impl Oban.Worker
diff --git a/lib/pleroma/workers/receiver_worker.ex b/lib/pleroma/workers/receiver_worker.ex
@@ -52,7 +52,8 @@ defmodule Pleroma.Workers.ReceiverWorker do
{:error, {:reject, reason}} -> {:cancel, reason}
{:signature, false} -> {:cancel, :invalid_signature}
{:error, {:error, reason = "Object has been deleted"}} -> {:cancel, reason}
- e -> e
+ {:error, _} = e -> e
+ e -> {:error, e}
end
end
end
diff --git a/lib/pleroma/workers/remote_fetcher_worker.ex b/lib/pleroma/workers/remote_fetcher_worker.ex
@@ -9,7 +9,25 @@ defmodule Pleroma.Workers.RemoteFetcherWorker do
@impl Oban.Worker
def perform(%Job{args: %{"op" => "fetch_remote", "id" => id} = args}) do
- {:ok, _object} = Fetcher.fetch_object_from_id(id, depth: args["depth"])
+ case Fetcher.fetch_object_from_id(id, depth: args["depth"]) do
+ {:ok, _object} ->
+ :ok
+
+ {:error, :forbidden} ->
+ {:discard, :forbidden}
+
+ {:error, :not_found} ->
+ {:discard, :not_found}
+
+ {:error, :allowed_depth} ->
+ {:discard, :allowed_depth}
+
+ {:error, _} = e ->
+ e
+
+ e ->
+ {:error, e}
+ end
end
@impl Oban.Worker
diff --git a/lib/pleroma/workers/rich_media_expiration_worker.ex b/lib/pleroma/workers/rich_media_expiration_worker.ex
@@ -0,0 +1,15 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Workers.RichMediaExpirationWorker do
+ alias Pleroma.Web.RichMedia.Card
+
+ use Oban.Worker,
+ queue: :rich_media_expiration
+
+ @impl Oban.Worker
+ def perform(%Job{args: %{"url" => url} = _args}) do
+ Card.delete(url)
+ end
+end
diff --git a/mix.exs b/mix.exs
@@ -4,12 +4,13 @@ defmodule Pleroma.Mixfile do
def project do
[
app: :pleroma,
- version: version("2.6.51"),
+ version: version("2.6.52"),
elixir: "~> 1.13",
elixirc_paths: elixirc_paths(Mix.env()),
compilers: Mix.compilers(),
elixirc_options: [warnings_as_errors: warnings_as_errors()],
xref: [exclude: [:eldap]],
+ dialyzer: [plt_add_apps: [:mix, :eldap]],
start_permanent: Mix.env() == :prod,
aliases: aliases(),
deps: deps(),
@@ -136,7 +137,7 @@ defmodule Pleroma.Mixfile do
{:calendar, "~> 1.0"},
{:cachex, "~> 3.2"},
{:poison, "~> 3.0", override: true},
- {:tesla, "~> 1.4.0", override: true},
+ {:tesla, "~> 1.8.0"},
{:castore, "~> 0.1"},
{:cowlib, "~> 2.9", override: true},
{:gun, "~> 2.0.0-rc.1", override: true},
@@ -156,11 +157,11 @@ defmodule Pleroma.Mixfile do
{:phoenix_swoosh, "~> 1.1"},
{:gen_smtp, "~> 0.13"},
{:ex_syslogger, "~> 1.4"},
- {:floki, "~> 0.27"},
+ {:floki, "~> 0.35"},
{:timex, "~> 3.6"},
{:ueberauth, "~> 0.4"},
{:linkify, "~> 0.5.3"},
- {:http_signatures, "~> 0.1.1"},
+ {:http_signatures, "~> 0.1.2"},
{:telemetry, "~> 1.0.0", override: true},
{:poolboy, "~> 1.5"},
{:prom_ex, "~> 1.9"},
@@ -184,6 +185,10 @@ defmodule Pleroma.Mixfile do
{:vix, "~> 0.26.0"},
{:elixir_make, "~> 0.7.7", override: true},
{:blurhash, "~> 0.1.0", hex: :rinpatch_blurhash},
+ {:exile,
+ git: "https://github.com/akash-akya/exile.git",
+ ref: "be87c33b02a7c3c5d22d2ece01fbd462355b28ef"},
+ {:bandit, "~> 1.2"},
## dev & test
{:ex_doc, "~> 0.22", only: :dev, runtime: false},
@@ -194,7 +199,8 @@ defmodule Pleroma.Mixfile do
{:hackney, "~> 1.18.0", override: true},
{:mox, "~> 1.0", only: :test},
{:websockex, "~> 0.4.3", only: :test},
- {:benchee, "~> 1.0", only: :benchmark}
+ {:benchee, "~> 1.0", only: :benchmark},
+ {:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false}
] ++ oauth_deps()
end
diff --git a/mix.lock b/mix.lock
@@ -1,19 +1,20 @@
%{
"accept": {:hex, :accept, "0.3.5", "b33b127abca7cc948bbe6caa4c263369abf1347cfa9d8e699c6d214660f10cd1", [:rebar3], [], "hexpm", "11b18c220bcc2eab63b5470c038ef10eb6783bcb1fcdb11aa4137defa5ac1bb8"},
+ "bandit": {:hex, :bandit, "1.2.1", "aa485b4ac175065b8e0fb5864ddd5dd7b50d52336b36f61c82f484c3718b3d15", [:mix], [{:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "27393e590a407f1b7d51c5fee4737f139fe224a30449ce25061eac70f763896b"},
"base62": {:hex, :base62, "1.2.2", "85c6627eb609317b70f555294045895ffaaeb1758666ab9ef9ca38865b11e629", [:mix], [{:custom_base, "~> 0.2.1", [hex: :custom_base, repo: "hexpm", optional: false]}], "hexpm", "d41336bda8eaa5be197f1e4592400513ee60518e5b9f4dcf38f4b4dae6f377bb"},
"bbcode_pleroma": {:hex, :bbcode_pleroma, "0.2.0", "d36f5bca6e2f62261c45be30fa9b92725c0655ad45c99025cb1c3e28e25803ef", [:mix], [{:nimble_parsec, "~> 0.5", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "19851074419a5fedb4ef49e1f01b30df504bb5dbb6d6adfc135238063bebd1c3"},
"bcrypt_elixir": {:hex, :bcrypt_elixir, "2.3.1", "5114d780459a04f2b4aeef52307de23de961b69e13a5cd98a911e39fda13f420", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "42182d5f46764def15bf9af83739e3bf4ad22661b1c34fc3e88558efced07279"},
- "benchee": {:hex, :benchee, "1.1.0", "f3a43817209a92a1fade36ef36b86e1052627fd8934a8b937ac9ab3a76c43062", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.0", [hex: :statistex, repo: "hexpm", optional: false]}], "hexpm", "7da57d545003165a012b587077f6ba90b89210fd88074ce3c60ce239eb5e6d93"},
+ "benchee": {:hex, :benchee, "1.3.0", "f64e3b64ad3563fa9838146ddefb2d2f94cf5b473bdfd63f5ca4d0657bf96694", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.0", [hex: :statistex, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "34f4294068c11b2bd2ebf2c59aac9c7da26ffa0068afdf3419f1b176e16c5f81"},
"blurhash": {:hex, :rinpatch_blurhash, "0.1.0", "01a888b0f5f1f382ab52e4396f01831cbe8486ea5828604c90f4dac533d39a4b", [:mix], [{:mogrify, "~> 0.8.0", [hex: :mogrify, repo: "hexpm", optional: true]}], "hexpm", "19911a5dcbb0acb9710169a72f702bce6cb048822b12de566ccd82b2cc42b907"},
- "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"},
+ "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
"cachex": {:hex, :cachex, "3.6.0", "14a1bfbeee060dd9bec25a5b6f4e4691e3670ebda28c8ba2884b12fe30b36bf8", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:jumper, "~> 1.0", [hex: :jumper, repo: "hexpm", optional: false]}, {:sleeplocks, "~> 1.1", [hex: :sleeplocks, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm", "ebf24e373883bc8e0c8d894a63bbe102ae13d918f790121f5cfe6e485cc8e2e2"},
"calendar": {:hex, :calendar, "1.0.0", "f52073a708528482ec33d0a171954ca610fe2bd28f1e871f247dc7f1565fa807", [:mix], [{:tzdata, "~> 0.5.20 or ~> 0.1.201603 or ~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "990e9581920c82912a5ee50e62ff5ef96da6b15949a2ee4734f935fdef0f0a6f"},
"captcha": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git", "90f6ce7672f70f56708792a98d98bd05176c9176", [ref: "90f6ce7672f70f56708792a98d98bd05176c9176"]},
"castore": {:hex, :castore, "0.1.22", "4127549e411bedd012ca3a308dede574f43819fe9394254ca55ab4895abfa1a2", [:mix], [], "hexpm", "c17576df47eb5aa1ee40cc4134316a99f5cad3e215d5c77b8dd3cfef12a22cac"},
"cc_precompiler": {:hex, :cc_precompiler, "0.1.9", "e8d3364f310da6ce6463c3dd20cf90ae7bbecbf6c5203b98bf9b48035592649b", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "9dcab3d0f3038621f1601f13539e7a9ee99843862e66ad62827b0c42b2f58a54"},
- "certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"},
+ "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"},
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
- "comeonin": {:hex, :comeonin, "5.3.3", "2c564dac95a35650e9b6acfe6d2952083d8a08e4a89b93a481acb552b325892e", [:mix], [], "hexpm", "3e38c9c2cb080828116597ca8807bb482618a315bfafd98c90bc22a821cc84df"},
+ "comeonin": {:hex, :comeonin, "5.4.0", "246a56ca3f41d404380fc6465650ddaa532c7f98be4bda1b4656b3a37cc13abe", [:mix], [], "hexpm", "796393a9e50d01999d56b7b8420ab0481a7538d0caf80919da493b4a6e51faf1"},
"concurrent_limiter": {:hex, :concurrent_limiter, "0.1.1", "43ae1dc23edda1ab03dd66febc739c4ff710d047bb4d735754909f9a474ae01c", [:mix], [{:telemetry, "~> 0.3", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "53968ff238c0fbb4d7ed76ddb1af0be6f3b2f77909f6796e249e737c505a16eb"},
"connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"},
"cors_plug": {:hex, :cors_plug, "2.0.3", "316f806d10316e6d10f09473f19052d20ba0a0ce2a1d910ddf57d663dac402ae", [:mix], [{:plug, "~> 1.8", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "ee4ae1418e6ce117fc42c2ba3e6cbdca4e95ecd2fe59a05ec6884ca16d469aea"},
@@ -21,92 +22,95 @@
"cowboy": {:hex, :cowboy, "2.10.0", "ff9ffeff91dae4ae270dd975642997afe2a1179d94b1887863e43f681a203e26", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "3afdccb7183cc6f143cb14d3cf51fa00e53db9ec80cdcd525482f5e99bc41d6b"},
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
"cowlib": {:hex, :cowlib, "2.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"},
- "credo": {:hex, :credo, "1.7.0", "6119bee47272e85995598ee04f2ebbed3e947678dee048d10b5feca139435f75", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "6839fcf63d1f0d1c0f450abc8564a57c43d644077ab96f2934563e68b8a769d7"},
+ "credo": {:hex, :credo, "1.7.3", "05bb11eaf2f2b8db370ecaa6a6bda2ec49b2acd5e0418bc106b73b07128c0436", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "35ea675a094c934c22fb1dca3696f3c31f2728ae6ef5a53b5d648c11180a4535"},
"crontab": {:hex, :crontab, "1.1.8", "2ce0e74777dfcadb28a1debbea707e58b879e6aa0ffbf9c9bb540887bce43617", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"},
"custom_base": {:hex, :custom_base, "0.2.1", "4a832a42ea0552299d81652aa0b1f775d462175293e99dfbe4d7dbaab785a706", [:mix], [], "hexpm", "8df019facc5ec9603e94f7270f1ac73ddf339f56ade76a721eaa57c1493ba463"},
"db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"},
"decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
"deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"},
+ "dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"},
"earmark": {:hex, :earmark, "1.4.22", "ea3e45c6359446dc308be0a64ce82a03260d973de7d0625a762e6d352ff57958", [:mix], [{:earmark_parser, "~> 1.4.23", [hex: :earmark_parser, repo: "hexpm", optional: false]}], "hexpm", "1caf5145665a42fd76d5317286b0c171861fb1c04f86ab103dde76868814fdfb"},
- "earmark_parser": {:hex, :earmark_parser, "1.4.32", "fa739a0ecfa34493de19426681b23f6814573faee95dfd4b4aafe15a7b5b32c6", [:mix], [], "hexpm", "b8b0dd77d60373e77a3d7e8afa598f325e49e8663a51bcc2b88ef41838cca755"},
+ "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"},
"eblurhash": {:git, "https://github.com/zotonic/eblurhash.git", "bc37ceb426ef021ee9927fb249bb93f7059194ab", [ref: "bc37ceb426ef021ee9927fb249bb93f7059194ab"]},
- "ecto": {:hex, :ecto, "3.10.3", "eb2ae2eecd210b4eb8bece1217b297ad4ff824b4384c0e3fdd28aaf96edd6135", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "44bec74e2364d491d70f7e42cd0d690922659d329f6465e89feb8a34e8cd3433"},
+ "ecto": {:hex, :ecto, "3.11.1", "4b4972b717e7ca83d30121b12998f5fcdc62ba0ed4f20fd390f16f3270d85c3e", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ebd3d3772cd0dfcd8d772659e41ed527c28b2a8bde4b00fe03e0463da0f1983b"},
"ecto_enum": {:hex, :ecto_enum, "1.4.0", "d14b00e04b974afc69c251632d1e49594d899067ee2b376277efd8233027aec8", [:mix], [{:ecto, ">= 3.0.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "> 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:mariaex, ">= 0.0.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "8fb55c087181c2b15eee406519dc22578fa60dd82c088be376d0010172764ee4"},
- "ecto_psql_extras": {:hex, :ecto_psql_extras, "0.7.14", "7a20cfe913b0476542b43870e67386461258734896035e3f284039fd18bd4c4c", [:mix], [{:ecto_sql, "~> 3.7", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0", [hex: :postgrex, repo: "hexpm", optional: false]}, {:table_rex, "~> 3.1.1", [hex: :table_rex, repo: "hexpm", optional: false]}], "hexpm", "22f5f98592dd597db9416fcef00effae0787669fdcb6faf447e982b553798e98"},
- "ecto_sql": {:hex, :ecto_sql, "3.10.2", "6b98b46534b5c2f8b8b5f03f126e75e2a73c64f3c071149d32987a5378b0fdbd", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.10.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "68c018debca57cb9235e3889affdaec7a10616a4e3a80c99fa1d01fdafaa9007"},
+ "ecto_psql_extras": {:hex, :ecto_psql_extras, "0.7.15", "0fc29dbae0e444a29bd6abeee4cf3c4c037e692a272478a234a1cc765077dbb1", [:mix], [{:ecto_sql, "~> 3.7", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0", [hex: :postgrex, repo: "hexpm", optional: false]}, {:table_rex, "~> 3.1.1 or ~> 4.0.0", [hex: :table_rex, repo: "hexpm", optional: false]}], "hexpm", "b6127f3a5c6fc3d84895e4768cc7c199f22b48b67d6c99b13fbf4a374e73f039"},
+ "ecto_sql": {:hex, :ecto_sql, "3.11.1", "e9abf28ae27ef3916b43545f9578b4750956ccea444853606472089e7d169470", [:mix], [{:db_connection, "~> 2.5 or ~> 2.4.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ce14063ab3514424276e7e360108ad6c2308f6d88164a076aac8a387e1fea634"},
"eimp": {:hex, :eimp, "1.0.14", "fc297f0c7e2700457a95a60c7010a5f1dcb768a083b6d53f49cd94ab95a28f22", [:rebar3], [{:p1_utils, "1.0.18", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "501133f3112079b92d9e22da8b88bf4f0e13d4d67ae9c15c42c30bd25ceb83b6"},
- "elixir_make": {:hex, :elixir_make, "0.7.7", "7128c60c2476019ed978210c245badf08b03dbec4f24d05790ef791da11aa17c", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "5bc19fff950fad52bbe5f211b12db9ec82c6b34a9647da0c2224b8b8464c7e6c"},
+ "elixir_make": {:hex, :elixir_make, "0.7.8", "505026f266552ee5aabca0b9f9c229cbb496c689537c9f922f3eb5431157efc7", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.0", [hex: :certifi, repo: "hexpm", optional: true]}], "hexpm", "7a71945b913d37ea89b06966e1342c85cfe549b15e6d6d081e8081c493062c07"},
+ "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
"esbuild": {:hex, :esbuild, "0.5.0", "d5bb08ff049d7880ee3609ed5c4b864bd2f46445ea40b16b4acead724fb4c4a3", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "f183a0b332d963c4cfaf585477695ea59eef9a6f2204fdd0efa00e099694ffe5"},
"eternal": {:hex, :eternal, "1.2.2", "d1641c86368de99375b98d183042dd6c2b234262b8d08dfd72b9eeaafc2a1abd", [:mix], [], "hexpm", "2c9fe32b9c3726703ba5e1d43a1d255a4f3f2d8f8f9bc19f094c7cb1a7a9e782"},
"ex_aws": {:hex, :ex_aws, "2.1.9", "dc4865ecc20a05190a34a0ac5213e3e5e2b0a75a0c2835e923ae7bfeac5e3c31", [:mix], [{:configparser_ex, "~> 4.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.6", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "3e6c776703c9076001fbe1f7c049535f042cb2afa0d2cbd3b47cbc4e92ac0d10"},
- "ex_aws_s3": {:hex, :ex_aws_s3, "2.4.0", "ce8decb6b523381812798396bc0e3aaa62282e1b40520125d1f4eff4abdff0f4", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "85dda6e27754d94582869d39cba3241d9ea60b6aa4167f9c88e309dc687e56bb"},
+ "ex_aws_s3": {:hex, :ex_aws_s3, "2.5.3", "422468e5c3e1a4da5298e66c3468b465cfd354b842e512cb1f6fbbe4e2f5bdaf", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "4f09dd372cc386550e484808c5ac5027766c8d0cd8271ccc578b82ee6ef4f3b8"},
"ex_const": {:hex, :ex_const, "0.2.4", "d06e540c9d834865b012a17407761455efa71d0ce91e5831e86881b9c9d82448", [:mix], [], "hexpm", "96fd346610cc992b8f896ed26a98be82ac4efb065a0578f334a32d60a3ba9767"},
- "ex_doc": {:hex, :ex_doc, "0.29.4", "6257ecbb20c7396b1fe5accd55b7b0d23f44b6aa18017b415cb4c2b91d997729", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "2c6699a737ae46cb61e4ed012af931b57b699643b24dabe2400a8168414bc4f5"},
+ "ex_doc": {:hex, :ex_doc, "0.31.1", "8a2355ac42b1cc7b2379da9e40243f2670143721dd50748bf6c3b1184dae2089", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.1", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "3178c3a407c557d8343479e1ff117a96fd31bafe52a039079593fb0524ef61b0"},
"ex_machina": {:hex, :ex_machina, "2.7.0", "b792cc3127fd0680fecdb6299235b4727a4944a09ff0fa904cc639272cd92dc7", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "419aa7a39bde11894c87a615c4ecaa52d8f107bbdd81d810465186f783245bf8"},
"ex_syslogger": {:hex, :ex_syslogger, "1.5.2", "72b6aa2d47a236e999171f2e1ec18698740f40af0bd02c8c650bf5f1fd1bac79", [:mix], [{:poison, ">= 1.5.0", [hex: :poison, repo: "hexpm", optional: true]}, {:syslog, "~> 1.1.0", [hex: :syslog, repo: "hexpm", optional: false]}], "hexpm", "ab9fab4136dbc62651ec6f16fa4842f10cf02ab4433fa3d0976c01be99398399"},
- "expo": {:hex, :expo, "0.4.1", "1c61d18a5df197dfda38861673d392e642649a9cef7694d2f97a587b2cfb319b", [:mix], [], "hexpm", "2ff7ba7a798c8c543c12550fa0e2cbc81b95d4974c65855d8d15ba7b37a1ce47"},
- "fast_html": {:hex, :fast_html, "2.0.5", "c61760340606c1077ff1f196f17834056cb1dd3d5cb92a9f2cabf28bc6221c3c", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}], "hexpm", "605f4f4829443c14127694ebabb681778712ceecb4470ec32aa31012330e6506"},
+ "exile": {:git, "https://github.com/akash-akya/exile.git", "be87c33b02a7c3c5d22d2ece01fbd462355b28ef", [ref: "be87c33b02a7c3c5d22d2ece01fbd462355b28ef"]},
+ "expo": {:hex, :expo, "0.5.1", "249e826a897cac48f591deba863b26c16682b43711dd15ee86b92f25eafd96d9", [:mix], [], "hexpm", "68a4233b0658a3d12ee00d27d37d856b1ba48607e7ce20fd376958d0ba6ce92b"},
+ "fast_html": {:hex, :fast_html, "2.2.0", "6c5ef1be087a4ed613b0379c13f815c4d11742b36b67bb52cee7859847c84520", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}], "hexpm", "064c4f23b4a6168f9187dac8984b056f2c531bb0787f559fd6a8b34b38aefbae"},
"fast_sanitize": {:hex, :fast_sanitize, "0.2.3", "67b93dfb34e302bef49fec3aaab74951e0f0602fd9fa99085987af05bd91c7a5", [:mix], [{:fast_html, "~> 2.0", [hex: :fast_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.8", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "e8ad286d10d0386e15d67d0ee125245ebcfbc7d7290b08712ba9013c8c5e56e2"},
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
- "finch": {:hex, :finch, "0.16.0", "40733f02c89f94a112518071c0a91fe86069560f5dbdb39f9150042f44dcfb1a", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f660174c4d519e5fec629016054d60edd822cdfe2b7270836739ac2f97735ec5"},
+ "finch": {:hex, :finch, "0.18.0", "944ac7d34d0bd2ac8998f79f7a811b21d87d911e77a786bc5810adb75632ada4", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69f5045b042e531e53edc2574f15e25e735b522c37e2ddb766e15b979e03aa65"},
"flake_id": {:hex, :flake_id, "0.1.0", "7716b086d2e405d09b647121a166498a0d93d1a623bead243e1f74216079ccb3", [:mix], [{:base62, "~> 1.2", [hex: :base62, repo: "hexpm", optional: false]}, {:ecto, ">= 2.0.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "31fc8090fde1acd267c07c36ea7365b8604055f897d3a53dd967658c691bd827"},
- "floki": {:hex, :floki, "0.34.3", "5e2dcaec5d7c228ce5b1d3501502e308b2d79eb655e4191751a1fe491c37feac", [:mix], [], "hexpm", "9577440eea5b97924b4bf3c7ea55f7b8b6dce589f9b28b096cc294a8dc342341"},
+ "floki": {:hex, :floki, "0.35.2", "87f8c75ed8654b9635b311774308b2760b47e9a579dabf2e4d5f1e1d42c39e0b", [:mix], [], "hexpm", "6b05289a8e9eac475f644f09c2e4ba7e19201fd002b89c28c1293e7bd16773d9"},
"gen_smtp": {:hex, :gen_smtp, "0.15.0", "9f51960c17769b26833b50df0b96123605a8024738b62db747fece14eb2fbfcc", [:rebar3], [], "hexpm", "29bd14a88030980849c7ed2447b8db6d6c9278a28b11a44cafe41b791205440f"},
- "gettext": {:hex, :gettext, "0.22.2", "6bfca374de34ecc913a28ba391ca184d88d77810a3e427afa8454a71a51341ac", [:mix], [{:expo, "~> 0.4.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "8a2d389673aea82d7eae387e6a2ccc12660610080ae7beb19452cfdc1ec30f60"},
+ "gettext": {:hex, :gettext, "0.24.0", "6f4d90ac5f3111673cbefc4ebee96fe5f37a114861ab8c7b7d5b30a1108ce6d8", [:mix], [{:expo, "~> 0.5.1", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "bdf75cdfcbe9e4622dd18e034b227d77dd17f0f133853a1c73b97b3d6c770e8b"},
"gun": {:hex, :gun, "2.0.1", "160a9a5394800fcba41bc7e6d421295cf9a7894c2252c0678244948e3336ad73", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "a10bc8d6096b9502205022334f719cc9a08d9adcfbfc0dbee9ef31b56274a20b"},
- "hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~>2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"},
+ "hackney": {:hex, :hackney, "1.18.2", "d7ff544ddae5e1cb49e9cf7fa4e356d7f41b283989a1c304bfc47a8cc1cf966f", [:rebar3], [{:certifi, "~>2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "af94d5c9f97857db257090a4a10e5426ecb6f4918aa5cc666798566ae14b65fd"},
"hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"},
"html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"},
- "http_signatures": {:hex, :http_signatures, "0.1.1", "ca7ebc1b61542b163644c8c3b1f0e0f41037d35f2395940d3c6c7deceab41fd8", [:mix], [], "hexpm", "cc3b8a007322cc7b624c0c15eec49ee58ac977254ff529a3c482f681465942a3"},
+ "http_signatures": {:hex, :http_signatures, "0.1.2", "ed1cc7043abcf5bb4f30d68fb7bad9d618ec1a45c4ff6c023664e78b67d9c406", [:mix], [], "hexpm", "f08aa9ac121829dae109d608d83c84b940ef2f183ae50f2dd1e9a8bc619d8be7"},
"httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"},
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
- "inet_cidr": {:hex, :inet_cidr, "1.0.4", "a05744ab7c221ca8e395c926c3919a821eb512e8f36547c062f62c4ca0cf3d6e", [:mix], [], "hexpm", "64a2d30189704ae41ca7dbdd587f5291db5d1dda1414e0774c29ffc81088c1bc"},
+ "inet_cidr": {:hex, :inet_cidr, "1.0.8", "d26bb7bdbdf21ae401ead2092bf2bb4bf57fe44a62f5eaa5025280720ace8a40", [:mix], [], "hexpm", "d5b26da66603bb56c933c65214c72152f0de9a6ea53618b56d63302a68f6a90e"},
"jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"},
"joken": {:hex, :joken, "2.6.0", "b9dd9b6d52e3e6fcb6c65e151ad38bf4bc286382b5b6f97079c47ade6b1bcc6a", [:mix], [{:jose, "~> 1.11.5", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "5a95b05a71cd0b54abd35378aeb1d487a23a52c324fa7efdffc512b655b5aaa7"},
- "jose": {:hex, :jose, "1.11.5", "3bc2d75ffa5e2c941ca93e5696b54978323191988eb8d225c2e663ddfefd515e", [:mix, :rebar3], [], "hexpm", "dcd3b215bafe02ea7c5b23dafd3eb8062a5cd8f2d904fd9caa323d37034ab384"},
- "jumper": {:hex, :jumper, "1.0.1", "3c00542ef1a83532b72269fab9f0f0c82bf23a35e27d278bfd9ed0865cecabff", [:mix], [], "hexpm", "318c59078ac220e966d27af3646026db9b5a5e6703cb2aa3e26bcfaba65b7433"},
+ "jose": {:hex, :jose, "1.11.6", "613fda82552128aa6fb804682e3a616f4bc15565a048dabd05b1ebd5827ed965", [:mix, :rebar3], [], "hexpm", "6275cb75504f9c1e60eeacb771adfeee4905a9e182103aa59b53fed651ff9738"},
+ "jumper": {:hex, :jumper, "1.0.2", "68cdcd84472a00ac596b4e6459a41b3062d4427cbd4f1e8c8793c5b54f1406a7", [:mix], [], "hexpm", "9b7782409021e01ab3c08270e26f36eb62976a38c1aa64b2eaf6348422f165e1"},
"linkify": {:hex, :linkify, "0.5.3", "5f8143d8f61f5ff08d3aeeff47ef6509492b4948d8f08007fbf66e4d2246a7f2", [:mix], [], "hexpm", "3ef35a1377d47c25506e07c1c005ea9d38d700699d92ee92825f024434258177"},
"majic": {:hex, :majic, "1.0.0", "37e50648db5f5c2ff0c9fb46454d034d11596c03683807b9fb3850676ffdaab3", [:make, :mix], [{:elixir_make, "~> 0.6.1", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "7905858f76650d49695f14ea55cd9aaaee0c6654fa391671d4cf305c275a0a9e"},
"makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"},
"makeup_elixir": {:hex, :makeup_elixir, "0.14.1", "4f0e96847c63c17841d42c08107405a005a2680eb9c7ccadfd757bd31dabccfb", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f2438b1a80eaec9ede832b5c41cd4f373b38fd7aa33e3b22d9db79e640cbde11"},
- "makeup_erlang": {:hex, :makeup_erlang, "0.1.2", "ad87296a092a46e03b7e9b0be7631ddcf64c790fa68a9ef5323b6cbb36affc72", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f3f5a1ca93ce6e092d92b6d9c049bcda58a3b617a8d888f8e7231c85630e8108"},
+ "makeup_erlang": {:hex, :makeup_erlang, "0.1.3", "d684f4bac8690e70b06eb52dad65d26de2eefa44cd19d64a8095e1417df7c8fd", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "b78dc853d2e670ff6390b605d807263bf606da3c82be37f9d7f68635bd886fc9"},
"meck": {:hex, :meck, "0.9.2", "85ccbab053f1db86c7ca240e9fc718170ee5bda03810a6292b5306bf31bae5f5", [:rebar3], [], "hexpm", "81344f561357dc40a8344afa53767c32669153355b626ea9fcbc8da6b3045826"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
"mime": {:hex, :mime, "1.6.0", "dabde576a497cef4bbdd60aceee8160e02a6c89250d6c0b29e56c0dfb00db3d2", [:mix], [], "hexpm", "31a1a8613f8321143dde1dafc36006a17d28d02bdfecb9e95a880fa7aabd19a7"},
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
- "mint": {:hex, :mint, "1.5.1", "8db5239e56738552d85af398798c80648db0e90f343c8469f6c6d8898944fb6f", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "4a63e1e76a7c3956abd2c72f370a0d0aecddc3976dea5c27eccbecfa5e7d5b1e"},
+ "mint": {:hex, :mint, "1.5.2", "4805e059f96028948870d23d7783613b7e6b0e2fb4e98d720383852a760067fd", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "d77d9e9ce4eb35941907f1d3df38d8f750c357865353e21d335bdcdf6d892a02"},
"mochiweb": {:hex, :mochiweb, "2.18.0", "eb55f1db3e6e960fac4e6db4e2db9ec3602cc9f30b86cd1481d56545c3145d2e", [:rebar3], [], "hexpm"},
"mock": {:hex, :mock, "0.3.8", "7046a306b71db2488ef54395eeb74df0a7f335a7caca4a3d3875d1fc81c884dd", [:mix], [{:meck, "~> 0.9.2", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "7fa82364c97617d79bb7d15571193fc0c4fe5afd0c932cef09426b3ee6fe2022"},
"mogrify": {:hex, :mogrify, "0.8.0", "3506f3ca3f7b95a155f3b4ef803b5db176f5a0633723e3fe85e0d6399e3b11c8", [:mix], [], "hexpm", "2278d245f07056ea3b586e98801e933695147066fa4cf563f552c1b4f0ff8ad9"},
- "mox": {:hex, :mox, "1.0.2", "dc2057289ac478b35760ba74165b4b3f402f68803dd5aecd3bfd19c183815d64", [:mix], [], "hexpm", "f9864921b3aaf763c8741b5b8e6f908f44566f1e427b2630e89e9a73b981fef2"},
- "nimble_options": {:hex, :nimble_options, "1.0.2", "92098a74df0072ff37d0c12ace58574d26880e522c22801437151a159392270e", [:mix], [], "hexpm", "fd12a8db2021036ce12a309f26f564ec367373265b53e25403f0ee697380f1b8"},
+ "mox": {:hex, :mox, "1.1.0", "0f5e399649ce9ab7602f72e718305c0f9cdc351190f72844599545e4996af73c", [:mix], [], "hexpm", "d44474c50be02d5b72131070281a5d3895c0e7a95c780e90bc0cfe712f633a13"},
+ "nimble_options": {:hex, :nimble_options, "1.1.0", "3b31a57ede9cb1502071fade751ab0c7b8dbe75a9a4c2b5bbb0943a690b63172", [:mix], [], "hexpm", "8bbbb3941af3ca9acc7835f5655ea062111c9c27bcac53e004460dfd19008a99"},
"nimble_parsec": {:hex, :nimble_parsec, "0.6.0", "32111b3bf39137144abd7ba1cce0914533b2d16ef35e8abc5ec8be6122944263", [:mix], [], "hexpm", "27eac315a94909d4dc68bc07a4a83e06c8379237c5ea528a9acff4ca1c873c52"},
"nimble_pool": {:hex, :nimble_pool, "0.2.6", "91f2f4c357da4c4a0a548286c84a3a28004f68f05609b4534526871a22053cde", [:mix], [], "hexpm", "1c715055095d3f2705c4e236c18b618420a35490da94149ff8b580a2144f653f"},
"nodex": {:git, "https://git.pleroma.social/pleroma/nodex", "cb6730f943cfc6aad674c92161be23a8411f15d1", [ref: "cb6730f943cfc6aad674c92161be23a8411f15d1"]},
"oban": {:hex, :oban, "2.13.6", "a0cb1bce3bd393770512231fb5a3695fa19fd3af10d7575bf73f837aee7abf43", [:mix], [{:ecto_sql, "~> 3.6", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3c1c5eb16f377b3cbbf2ea14be24d20e3d91285af9d1ac86260b7c2af5464887"},
- "octo_fetch": {:hex, :octo_fetch, "0.3.0", "89ff501d2ac0448556ff1931634a538fe6d6cd358ba827ce1747e6a42a46efbf", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "c07e44f2214ab153743b7b3182f380798d0b294b1f283811c1e30cff64096d3d"},
- "open_api_spex": {:hex, :open_api_spex, "3.17.3", "ada8e352eb786050dd639db2439d3316e92f3798eb2abd051f55bb9af825b37e", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "165db21a85ca83cffc8e7c8890f35b354eddda8255de7404a2848ed652b9f0fe"},
- "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"},
+ "octo_fetch": {:hex, :octo_fetch, "0.4.0", "074b5ecbc08be10b05b27e9db08bc20a3060142769436242702931c418695b19", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "cf8be6f40cd519d7000bb4e84adcf661c32e59369ca2827c4e20042eda7a7fc6"},
+ "open_api_spex": {:hex, :open_api_spex, "3.18.2", "8c855e83bfe8bf81603d919d6e892541eafece3720f34d1700b58024dadde247", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0 or ~> 4.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "aa3e6dcfc0ad6a02596b2172662da21c9dd848dac145ea9e603f54e3d81b8d2b"},
+ "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
"pbkdf2_elixir": {:hex, :pbkdf2_elixir, "1.2.1", "9cbe354b58121075bd20eb83076900a3832324b7dd171a6895fab57b6bb2752c", [:mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}], "hexpm", "d3b40a4a4630f0b442f19eca891fcfeeee4c40871936fed2f68e1c4faa30481f"},
"phoenix": {:hex, :phoenix, "1.7.10", "02189140a61b2ce85bb633a9b6fd02dff705a5f1596869547aeb2b2b95edd729", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "cf784932e010fd736d656d7fead6a584a4498efefe5b8227e9f383bf15bb79d0"},
- "phoenix_ecto": {:hex, :phoenix_ecto, "4.4.2", "b21bd01fdeffcfe2fab49e4942aa938b6d3e89e93a480d4aee58085560a0bc0d", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "70242edd4601d50b69273b057ecf7b684644c19ee750989fd555625ae4ce8f5d"},
+ "phoenix_ecto": {:hex, :phoenix_ecto, "4.4.3", "86e9878f833829c3f66da03d75254c155d91d72a201eb56ae83482328dc7ca93", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "d36c401206f3011fefd63d04e8ef626ec8791975d9d107f9a0817d426f61ac07"},
"phoenix_html": {:hex, :phoenix_html, "3.3.3", "380b8fb45912b5638d2f1d925a3771b4516b9a78587249cabe394e0a5d579dc9", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "923ebe6fec6e2e3b3e569dfbdc6560de932cd54b000ada0208b5f45024bdd76c"},
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.3", "7ff51c9b6609470f681fbea20578dede0e548302b0c8bdf338b5a753a4f045bf", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "f9470a0a8bae4f56430a23d42f977b5a6205fdba6559d76f932b876bfaec652d"},
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.3.3", "3a53772a6118d5679bf50fc1670505a290e32a1d195df9e069d8c53ab040c054", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "766796676e5f558dbae5d1bdb066849673e956005e3730dfd5affd7a6da4abac"},
"phoenix_live_view": {:hex, :phoenix_live_view, "0.19.5", "6e730595e8e9b8c5da230a814e557768828fd8dfeeb90377d2d8dbb52d4ec00a", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b2eaa0dd3cfb9bd7fb949b88217df9f25aed915e986a28ad5c8a0d054e7ca9d3"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"},
- "phoenix_swoosh": {:hex, :phoenix_swoosh, "1.2.0", "a544d83fde4a767efb78f45404a74c9e37b2a9c5ea3339692e65a6966731f935", [:mix], [{:finch, "~> 0.8", [hex: :finch, repo: "hexpm", optional: true]}, {:hackney, "~> 1.10", [hex: :hackney, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_view, "~> 1.0 or ~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:swoosh, "~> 1.5", [hex: :swoosh, repo: "hexpm", optional: false]}], "hexpm", "e88d117251e89a16b92222415a6d87b99a96747ddf674fc5c7631de734811dba"},
- "phoenix_template": {:hex, :phoenix_template, "1.0.3", "32de561eefcefa951aead30a1f94f1b5f0379bc9e340bb5c667f65f1edfa4326", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "16f4b6588a4152f3cc057b9d0c0ba7e82ee23afa65543da535313ad8d25d8e2c"},
+ "phoenix_swoosh": {:hex, :phoenix_swoosh, "1.2.1", "b74ccaa8046fbc388a62134360ee7d9742d5a8ae74063f34eb050279de7a99e1", [:mix], [{:finch, "~> 0.8", [hex: :finch, repo: "hexpm", optional: true]}, {:hackney, "~> 1.10", [hex: :hackney, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_view, "~> 1.0 or ~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:swoosh, "~> 1.5", [hex: :swoosh, repo: "hexpm", optional: false]}], "hexpm", "4000eeba3f9d7d1a6bf56d2bd56733d5cadf41a7f0d8ffe5bb67e7d667e204a2"},
+ "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"},
"phoenix_view": {:hex, :phoenix_view, "2.0.3", "4d32c4817fce933693741deeb99ef1392619f942633dde834a5163124813aad3", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "cd34049af41be2c627df99cd4eaa71fc52a328c0c3d8e7d4aa28f880c30e7f64"},
- "plug": {:hex, :plug, "1.15.1", "b7efd81c1a1286f13efb3f769de343236bd8b7d23b4a9f40d3002fc39ad8f74c", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "459497bd94d041d98d948054ec6c0b76feacd28eec38b219ca04c0de13c79d30"},
- "plug_cowboy": {:hex, :plug_cowboy, "2.6.1", "9a3bbfceeb65eff5f39dab529e5cd79137ac36e913c02067dba3963a26efe9b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "de36e1a21f451a18b790f37765db198075c25875c64834bcc82d90b309eb6613"},
+ "plug": {:hex, :plug, "1.15.3", "712976f504418f6dff0a3e554c40d705a9bcf89a7ccef92fc6a5ef8f16a30a97", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cc4365a3c010a56af402e0809208873d113e9c38c401cabd88027ef4f5c01fd2"},
+ "plug_cowboy": {:hex, :plug_cowboy, "2.6.2", "753611b23b29231fb916b0cdd96028084b12aff57bfd7b71781bd04b1dbeb5c9", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "951ed2433df22f4c97b85fdb145d4cee561f36b74854d64c06d896d7cd2921a7"},
"plug_crypto": {:hex, :plug_crypto, "2.0.0", "77515cc10af06645abbfb5e6ad7a3e9714f805ae118fa1a70205f80d2d70fe73", [:mix], [], "hexpm", "53695bae57cc4e54566d993eb01074e4d894b65a3766f1c43e2c61a1b0f45ea9"},
"plug_static_index_html": {:hex, :plug_static_index_html, "1.0.0", "840123d4d3975585133485ea86af73cb2600afd7f2a976f9f5fd8b3808e636a0", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "79fd4fcf34d110605c26560cbae8f23c603ec4158c08298bd4360fdea90bb5cf"},
"poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm", "fec8660eb7733ee4117b85f55799fd3833eb769a6df71ccf8903e8dc5447cfce"},
"poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"},
- "postgrex": {:hex, :postgrex, "0.17.3", "c92cda8de2033a7585dae8c61b1d420a1a1322421df84da9a82a6764580c503d", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "946cf46935a4fdca7a81448be76ba3503cff082df42c6ec1ff16a4bdfbfb098d"},
+ "postgrex": {:hex, :postgrex, "0.17.4", "5777781f80f53b7c431a001c8dad83ee167bcebcf3a793e3906efff680ab62b3", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "6458f7d5b70652bc81c3ea759f91736c16a31be000f306d3c64bcdfe9a18b3cc"},
"pot": {:hex, :pot, "1.0.2", "13abb849139fdc04ab8154986abbcb63bdee5de6ed2ba7e1713527e33df923dd", [:rebar3], [], "hexpm", "78fe127f5a4f5f919d6ea5a2a671827bd53eb9d37e5b4128c0ad3df99856c2e0"},
- "prom_ex": {:hex, :prom_ex, "1.9.0", "63e6dda6c05cdeec1f26c48443dcc38ffd2118b3665ae8d2bd0e5b79f2aea03e", [:mix], [{:absinthe, ">= 1.6.0", [hex: :absinthe, repo: "hexpm", optional: true]}, {:broadway, ">= 1.0.2", [hex: :broadway, repo: "hexpm", optional: true]}, {:ecto, ">= 3.5.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:finch, "~> 0.15", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}, {:oban, ">= 2.4.0", [hex: :oban, repo: "hexpm", optional: true]}, {:octo_fetch, "~> 0.3", [hex: :octo_fetch, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.5.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_live_view, ">= 0.14.0", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}, {:plug, ">= 1.12.1", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, "~> 2.5", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:telemetry, ">= 1.0.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}, {:telemetry_metrics_prometheus_core, "~> 1.0", [hex: :telemetry_metrics_prometheus_core, repo: "hexpm", optional: false]}, {:telemetry_poller, "~> 1.0", [hex: :telemetry_poller, repo: "hexpm", optional: false]}], "hexpm", "01f3d4f69ec93068219e686cc65e58a29c42bea5429a8ff4e2121f19db178ee6"},
+ "prom_ex": {:hex, :prom_ex, "1.9.0", "63e6dda6c05cdeec1f26c48443dcc38ffd2118b3665ae8d2bd0e5b79f2aea03e", [:mix], [{:absinthe, ">= 1.6.0", [hex: :absinthe, repo: "hexpm", optional: true]}, {:broadway, ">= 1.0.2", [hex: :broadway, repo: "hexpm", optional: true]}, {:ecto, ">= 3.5.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:finch, "~> 0.15", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}, {:oban, ">= 2.4.0", [hex: :oban, repo: "hexpm", optional: true]}, {:octo_fetch, "~> 0.3", [hex: :octo_fetch, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.5.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_live_view, ">= 0.14.0", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}, {:plug, ">= 1.12.1", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, "~> 2.5 or ~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:telemetry, ">= 1.0.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}, {:telemetry_metrics_prometheus_core, "~> 1.0", [hex: :telemetry_metrics_prometheus_core, repo: "hexpm", optional: false]}, {:telemetry_poller, "~> 1.0", [hex: :telemetry_poller, repo: "hexpm", optional: false]}], "hexpm", "01f3d4f69ec93068219e686cc65e58a29c42bea5429a8ff4e2121f19db178ee6"},
"prometheus": {:hex, :prometheus, "4.10.0", "792adbf0130ff61b5fa8826f013772af24b6e57b984445c8d602c8a0355704a1", [:mix, :rebar3], [{:quantile_estimator, "~> 0.2.1", [hex: :quantile_estimator, repo: "hexpm", optional: false]}], "hexpm", "2a99bb6dce85e238c7236fde6b0064f9834dc420ddbd962aac4ea2a3c3d59384"},
"prometheus_ecto": {:hex, :prometheus_ecto, "1.4.3", "3dd4da1812b8e0dbee81ea58bb3b62ed7588f2eae0c9e97e434c46807ff82311", [:mix], [{:ecto, "~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.1 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}], "hexpm", "8d66289f77f913b37eda81fd287340c17e61a447549deb28efc254532b2bed82"},
"prometheus_ex": {:git, "https://github.com/lanodan/prometheus.ex.git", "31f7fbe4b71b79ba27efc2a5085746c4011ceb8f", [branch: "fix/elixir-1.14"]},
@@ -115,29 +119,30 @@
"prometheus_plugs": {:hex, :prometheus_plugs, "1.1.5", "25933d48f8af3a5941dd7b621c889749894d8a1082a6ff7c67cc99dec26377c5", [:mix], [{:accept, "~> 0.1", [hex: :accept, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.1 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}, {:prometheus_process_collector, "~> 1.1", [hex: :prometheus_process_collector, repo: "hexpm", optional: true]}], "hexpm", "0273a6483ccb936d79ca19b0ab629aef0dba958697c94782bb728b920dfc6a79"},
"quantile_estimator": {:hex, :quantile_estimator, "0.2.1", "ef50a361f11b5f26b5f16d0696e46a9e4661756492c981f7b2229ef42ff1cd15", [:rebar3], [], "hexpm", "282a8a323ca2a845c9e6f787d166348f776c1d4a41ede63046d72d422e3da946"},
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
- "recon": {:hex, :recon, "2.5.3", "739107b9050ea683c30e96de050bc59248fd27ec147696f79a8797ff9fa17153", [:mix, :rebar3], [], "hexpm", "6c6683f46fd4a1dfd98404b9f78dcabc7fcd8826613a89dcb984727a8c3099d7"},
+ "recon": {:hex, :recon, "2.5.4", "05dd52a119ee4059fa9daa1ab7ce81bc7a8161a2f12e9d42e9d551ffd2ba901c", [:mix, :rebar3], [], "hexpm", "e9ab01ac7fc8572e41eb59385efeb3fb0ff5bf02103816535bacaedf327d0263"},
"remote_ip": {:git, "https://git.pleroma.social/pleroma/remote_ip.git", "b647d0deecaa3acb140854fe4bda5b7e1dc6d1c8", [ref: "b647d0deecaa3acb140854fe4bda5b7e1dc6d1c8"]},
"rustler": {:hex, :rustler, "0.30.0", "cefc49922132b072853fa9b0ca4dc2ffcb452f68fb73b779042b02d545e097fb", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:toml, "~> 0.6", [hex: :toml, repo: "hexpm", optional: false]}], "hexpm", "9ef1abb6a7dda35c47cfc649e6a5a61663af6cf842a55814a554a84607dee389"},
"sleeplocks": {:hex, :sleeplocks, "1.1.2", "d45aa1c5513da48c888715e3381211c859af34bee9b8290490e10c90bb6ff0ca", [:rebar3], [], "hexpm", "9fe5d048c5b781d6305c1a3a0f40bb3dfc06f49bf40571f3d2d0c57eaa7f59a5"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
"statistex": {:hex, :statistex, "1.0.0", "f3dc93f3c0c6c92e5f291704cf62b99b553253d7969e9a5fa713e5481cd858a5", [:mix], [], "hexpm", "ff9d8bee7035028ab4742ff52fc80a2aa35cece833cf5319009b52f1b5a86c27"},
- "sweet_xml": {:hex, :sweet_xml, "0.7.3", "debb256781c75ff6a8c5cbf7981146312b66f044a2898f453709a53e5031b45b", [:mix], [], "hexpm", "e110c867a1b3fe74bfc7dd9893aa851f0eed5518d0d7cad76d7baafd30e4f5ba"},
+ "sweet_xml": {:hex, :sweet_xml, "0.7.4", "a8b7e1ce7ecd775c7e8a65d501bc2cd933bff3a9c41ab763f5105688ef485d08", [:mix], [], "hexpm", "e7c4b0bdbf460c928234951def54fe87edf1a170f6896675443279e2dbeba167"},
"swoosh": {:hex, :swoosh, "1.10.3", "32f1531ee3fe4e82da8175c597bf3692938f8152eb981e0cbf57107b6c5924c1", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8b7167d93047bac6e1a1c367bf7d899cf2e4fea0592ee04a70673548ef6091b9"},
"syslog": {:hex, :syslog, "1.1.0", "6419a232bea84f07b56dc575225007ffe34d9fdc91abe6f1b2f254fd71d8efc2", [:rebar3], [], "hexpm", "4c6a41373c7e20587be33ef841d3de6f3beba08519809329ecc4d27b15b659e1"},
- "table_rex": {:hex, :table_rex, "3.1.1", "0c67164d1714b5e806d5067c1e96ff098ba7ae79413cc075973e17c38a587caa", [:mix], [], "hexpm", "678a23aba4d670419c23c17790f9dcd635a4a89022040df7d5d772cb21012490"},
+ "table_rex": {:hex, :table_rex, "4.0.0", "3c613a68ebdc6d4d1e731bc973c233500974ec3993c99fcdabb210407b90959b", [:mix], [], "hexpm", "c35c4d5612ca49ebb0344ea10387da4d2afe278387d4019e4d8111e815df8f55"},
"telemetry": {:hex, :telemetry, "1.0.0", "0f453a102cdf13d506b7c0ab158324c337c41f1cc7548f0bc0e130bbf0ae9452", [:rebar3], [], "hexpm", "73bc09fa59b4a0284efb4624335583c528e07ec9ae76aca96ea0673850aec57a"},
- "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"},
- "telemetry_metrics_prometheus_core": {:hex, :telemetry_metrics_prometheus_core, "1.1.0", "4e15f6d7dbedb3a4e3aed2262b7e1407f166fcb9c30ca3f96635dfbbef99965c", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "0dd10e7fe8070095df063798f82709b0a1224c31b8baf6278b423898d591a069"},
+ "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.2", "2caabe9344ec17eafe5403304771c3539f3b6e2f7fb6a6f602558c825d0d0bfb", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9b43db0dc33863930b9ef9d27137e78974756f5f198cae18409970ed6fa5b561"},
+ "telemetry_metrics_prometheus_core": {:hex, :telemetry_metrics_prometheus_core, "1.2.0", "b583c3f18508f5c5561b674d16cf5d9afd2ea3c04505b7d92baaeac93c1b8260", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "9cba950e1c4733468efbe3f821841f34ac05d28e7af7798622f88ecdbbe63ea3"},
"telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"},
- "tesla": {:hex, :tesla, "1.4.4", "bb89aa0c9745190930366f6a2ac612cdf2d0e4d7fff449861baa7875afd797b2", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.3", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, "~> 1.3", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "d5503a49f9dec1b287567ea8712d085947e247cb11b06bc54adb05bfde466457"},
+ "tesla": {:hex, :tesla, "1.8.0", "d511a4f5c5e42538d97eef7c40ec4f3e44effdc5068206f42ed859e09e51d1fd", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "10501f360cd926a309501287470372af1a6e1cbed0f43949203a4c13300bc79f"},
+ "thousand_island": {:hex, :thousand_island, "1.3.2", "bc27f9afba6e1a676dd36507d42e429935a142cf5ee69b8e3f90bff1383943cd", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "0e085b93012cd1057b378fce40cbfbf381ff6d957a382bfdd5eca1a98eec2535"},
"timex": {:hex, :timex, "3.7.7", "3ed093cae596a410759104d878ad7b38e78b7c2151c6190340835515d4a46b8a", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "0ec4b09f25fe311321f9fc04144a7e3affe48eb29481d7a5583849b6c4dfa0a7"},
"toml": {:hex, :toml, "0.7.0", "fbcd773caa937d0c7a02c301a1feea25612720ac3fa1ccb8bfd9d30d822911de", [:mix], [], "hexpm", "0690246a2478c1defd100b0c9b89b4ea280a22be9a7b313a8a058a2408a2fa70"},
"trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bd4fde4c15f3e993a999e019d64347489b91b7a9096af68b2bdadd192afa693f"},
"tzdata": {:hex, :tzdata, "1.0.5", "69f1ee029a49afa04ad77801febaf69385f3d3e3d1e4b56b9469025677b89a28", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "55519aa2a99e5d2095c1e61cc74c9be69688f8ab75c27da724eb8279ff402a5a"},
- "ueberauth": {:hex, :ueberauth, "0.10.5", "806adb703df87e55b5615cf365e809f84c20c68aa8c08ff8a416a5a6644c4b02", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "3efd1f31d490a125c7ed453b926f7c31d78b97b8a854c755f5c40064bf3ac9e1"},
+ "ueberauth": {:hex, :ueberauth, "0.10.7", "5a31cbe11e7ce5c7484d745dc9e1f11948e89662f8510d03c616de03df581ebd", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "0bccf73e2ffd6337971340832947ba232877aa8122dba4c95be9f729c8987377"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
- "unsafe": {:hex, :unsafe, "1.0.1", "a27e1874f72ee49312e0a9ec2e0b27924214a05e3ddac90e91727bc76f8613d8", [:mix], [], "hexpm", "6c7729a2d214806450d29766abc2afaa7a2cbecf415be64f36a6691afebb50e5"},
- "vix": {:hex, :vix, "0.26.0", "027f10b6969b759318be84bd0bd8c88af877445e4e41cf96a0460392cea5399c", [:make, :mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:cc_precompiler, "~> 0.1.4 or ~> 0.2", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.7.3 or ~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:kino, "~> 0.7", [hex: :kino, repo: "hexpm", optional: true]}], "hexpm", "71b0a79ae7f199cacfc8e679b0e4ba25ee47dc02e182c5b9097efb29fbe14efd"},
+ "unsafe": {:hex, :unsafe, "1.0.2", "23c6be12f6c1605364801f4b47007c0c159497d0446ad378b5cf05f1855c0581", [:mix], [], "hexpm", "b485231683c3ab01a9cd44cb4a79f152c6f3bb87358439c6f68791b85c2df675"},
+ "vix": {:hex, :vix, "0.26.0", "027f10b6969b759318be84bd0bd8c88af877445e4e41cf96a0460392cea5399c", [:make, :mix], [{:castore, "~> 1.0 or ~> 0.1", [hex: :castore, repo: "hexpm", optional: false]}, {:cc_precompiler, "~> 0.2 or ~> 0.1.4", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.8 or ~> 0.7.3", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:kino, "~> 0.7", [hex: :kino, repo: "hexpm", optional: true]}], "hexpm", "71b0a79ae7f199cacfc8e679b0e4ba25ee47dc02e182c5b9097efb29fbe14efd"},
"web_push_encryption": {:hex, :web_push_encryption, "0.3.1", "76d0e7375142dfee67391e7690e89f92578889cbcf2879377900b5620ee4708d", [:mix], [{:httpoison, "~> 1.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jose, "~> 1.11.1", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "4f82b2e57622fb9337559058e8797cb0df7e7c9790793bdc4e40bc895f70e2a2"},
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
"websock_adapter": {:hex, :websock_adapter, "0.5.5", "9dfeee8269b27e958a65b3e235b7e447769f66b5b5925385f5a569269164a210", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "4b977ba4a01918acbf77045ff88de7f6972c2a009213c515a445c48f224ffce9"},
diff --git a/priv/gettext/ja/LC_MESSAGES/errors.po b/priv/gettext/ja/LC_MESSAGES/errors.po
@@ -3,16 +3,16 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-09-18 09:07+0000\n"
-"PO-Revision-Date: 2021-09-19 09:45+0000\n"
-"Last-Translator: Ryo Ueno <r.ueno.nfive@gmail.com>\n"
+"PO-Revision-Date: 2024-02-16 11:49+0000\n"
+"Last-Translator: SyoBoN <syobon@syobon.net>\n"
"Language-Team: Japanese <https://translate.pleroma.social/projects/pleroma/"
-"pleroma/ja/>\n"
+"pleroma-backend-domain-errors/ja/>\n"
"Language: ja\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
-"X-Generator: Weblate 4.6.2\n"
+"X-Generator: Weblate 4.13.1\n"
## This file is a PO Template file.
##
@@ -25,11 +25,11 @@ msgstr ""
## effect: edit them in PO (`.po`) files instead.
## From Ecto.Changeset.cast/4
msgid "can't be blank"
-msgstr ""
+msgstr "空にすることはできません"
## From Ecto.Changeset.unique_constraint/3
msgid "has already been taken"
-msgstr ""
+msgstr "すでに使用されています"
## From Ecto.Changeset.put_change/3
msgid "is invalid"
@@ -37,7 +37,7 @@ msgstr ""
## From Ecto.Changeset.validate_format/3
msgid "has invalid format"
-msgstr ""
+msgstr "書式が正しくありません"
## From Ecto.Changeset.validate_subset/3
msgid "has an invalid entry"
@@ -45,7 +45,7 @@ msgstr ""
## From Ecto.Changeset.validate_exclusion/3
msgid "is reserved"
-msgstr ""
+msgstr "予約されています"
## From Ecto.Changeset.validate_confirmation/3
msgid "does not match confirmation"
@@ -61,23 +61,23 @@ msgstr ""
## From Ecto.Changeset.validate_length/3
msgid "should be %{count} character(s)"
msgid_plural "should be %{count} character(s)"
-msgstr[0] ""
+msgstr[0] "%{count}文字である必要があります"
msgid "should have %{count} item(s)"
msgid_plural "should have %{count} item(s)"
-msgstr[0] ""
+msgstr[0] "%{count}個の要素を含む必要があります"
msgid "should be at least %{count} character(s)"
msgid_plural "should be at least %{count} character(s)"
-msgstr[0] ""
+msgstr[0] "%{count}文字以上である必要があります"
msgid "should have at least %{count} item(s)"
msgid_plural "should have at least %{count} item(s)"
-msgstr[0] ""
+msgstr[0] "%{count}個以上の要素を含む必要があります"
msgid "should be at most %{count} character(s)"
msgid_plural "should be at most %{count} character(s)"
-msgstr[0] ""
+msgstr[0] "%{count}文字以下である必要があります"
msgid "should have at most %{count} item(s)"
msgid_plural "should have at most %{count} item(s)"
@@ -102,7 +102,7 @@ msgstr "%{number}でなければなりません"
#: lib/pleroma/web/common_api/common_api.ex:505
#, elixir-format
msgid "Account not found"
-msgstr "アカウントがありません"
+msgstr "アカウントが見つかりません"
#: lib/pleroma/web/common_api/common_api.ex:339
#, elixir-format
@@ -226,7 +226,7 @@ msgstr ""
#: lib/pleroma/web/common_api/utils.ex:414
#, elixir-format
msgid "Invalid password."
-msgstr ""
+msgstr "パスワードが間違っています。"
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:220
#, elixir-format
@@ -236,7 +236,7 @@ msgstr ""
#: lib/pleroma/web/twitter_api/twitter_api.ex:109
#, elixir-format
msgid "Kocaptcha service unavailable"
-msgstr ""
+msgstr "Kocaptchaが利用できません"
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:112
#, elixir-format
@@ -259,12 +259,12 @@ msgstr ""
#: lib/pleroma/web/feed/user_controller.ex:71 lib/pleroma/web/ostatus/ostatus_controller.ex:143
#, elixir-format
msgid "Not found"
-msgstr ""
+msgstr "見つかりません"
#: lib/pleroma/web/common_api/common_api.ex:331
#, elixir-format
msgid "Poll's author can't vote"
-msgstr ""
+msgstr "作成者は投票できません"
#: lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:20
#: lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:37 lib/pleroma/web/mastodon_api/controllers/poll_controller.ex:49
@@ -284,17 +284,17 @@ msgstr ""
#: lib/pleroma/web/common_api/activity_draft.ex:107
#, elixir-format
msgid "The message visibility must be direct"
-msgstr ""
+msgstr "公開範囲は「ダイレクト」でなければいけません"
#: lib/pleroma/web/common_api/utils.ex:573
#, elixir-format
msgid "The status is over the character limit"
-msgstr ""
+msgstr "文字数制限を超過しています"
#: lib/pleroma/plugs/ensure_public_or_authenticated_plug.ex:31
#, elixir-format
msgid "This resource requires authentication."
-msgstr ""
+msgstr "認証が必要です。"
#: lib/pleroma/plugs/rate_limiter/rate_limiter.ex:206
#, elixir-format
@@ -304,7 +304,7 @@ msgstr ""
#: lib/pleroma/web/common_api/common_api.ex:356
#, elixir-format
msgid "Too many choices"
-msgstr ""
+msgstr "選択肢が多すぎます"
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:443
#, elixir-format
@@ -320,7 +320,7 @@ msgstr ""
#: lib/pleroma/web/oauth/oauth_controller.ex:308
#, elixir-format
msgid "Your account is currently disabled"
-msgstr ""
+msgstr "あなたのアカウントは無効化されています"
#: lib/pleroma/web/oauth/oauth_controller.ex:183
#: lib/pleroma/web/oauth/oauth_controller.ex:331
@@ -331,23 +331,23 @@ msgstr ""
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:390
#, elixir-format
msgid "can't read inbox of %{nickname} as %{as_nickname}"
-msgstr ""
+msgstr "%{nickname}のinboxを%{as_nickname}として閲覧することはできません"
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:473
#, elixir-format
msgid "can't update outbox of %{nickname} as %{as_nickname}"
-msgstr ""
+msgstr "%{nickname}のoutboxを%{as_nickname}として更新することはできません"
#: lib/pleroma/web/common_api/common_api.ex:471
#, elixir-format
msgid "conversation is already muted"
-msgstr ""
+msgstr "この会話はミュート済みです"
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:314
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:492
#, elixir-format
msgid "error"
-msgstr ""
+msgstr "エラー"
#: lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex:32
#, elixir-format
@@ -357,7 +357,7 @@ msgstr ""
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:62
#, elixir-format
msgid "not found"
-msgstr ""
+msgstr "見つかりません"
#: lib/pleroma/web/oauth/oauth_controller.ex:394
#, elixir-format
@@ -382,28 +382,28 @@ msgstr ""
#: lib/pleroma/web/oauth/oauth_controller.ex:410
#, elixir-format
msgid "Failed to authenticate: %{message}."
-msgstr ""
+msgstr "認証に失敗しました: %{message}。"
#: lib/pleroma/web/oauth/oauth_controller.ex:441
#, elixir-format
msgid "Failed to set up user account."
-msgstr ""
+msgstr "ユーザーアカウントの設定に失敗しました。"
#: lib/pleroma/plugs/oauth_scopes_plug.ex:38
#, elixir-format
msgid "Insufficient permissions: %{permissions}."
-msgstr ""
+msgstr "%{permissions}の権限が必要です。"
#: lib/pleroma/plugs/uploaded_media.ex:104
#, elixir-format
msgid "Internal Error"
-msgstr ""
+msgstr "内部エラー"
#: lib/pleroma/web/oauth/fallback_controller.ex:22
#: lib/pleroma/web/oauth/fallback_controller.ex:29
#, elixir-format
msgid "Invalid Username/Password"
-msgstr ""
+msgstr "ユーザー名/パスワードが間違っています"
#: lib/pleroma/web/twitter_api/twitter_api.ex:118
#, elixir-format
@@ -423,7 +423,7 @@ msgstr ""
#: lib/pleroma/web/oauth/fallback_controller.ex:14
#, elixir-format
msgid "Unknown error, please check the details and try again."
-msgstr ""
+msgstr "不明なエラーです。詳細を確認し、再度試行してください。"
#: lib/pleroma/web/oauth/oauth_controller.ex:119
#: lib/pleroma/web/oauth/oauth_controller.ex:158
@@ -449,17 +449,17 @@ msgstr ""
#: lib/pleroma/web/twitter_api/twitter_api.ex:103
#, elixir-format
msgid "CAPTCHA Error"
-msgstr ""
+msgstr "CAPTCHAエラー"
#: lib/pleroma/web/common_api/common_api.ex:290
#, elixir-format
msgid "Could not add reaction emoji"
-msgstr ""
+msgstr "リアクションを追加できません"
#: lib/pleroma/web/common_api/common_api.ex:301
#, elixir-format
msgid "Could not remove reaction emoji"
-msgstr ""
+msgstr "リアクションを解除できません"
#: lib/pleroma/web/twitter_api/twitter_api.ex:129
#, elixir-format
@@ -469,7 +469,7 @@ msgstr ""
#: lib/pleroma/web/mastodon_api/controllers/list_controller.ex:92
#, elixir-format
msgid "List not found"
-msgstr ""
+msgstr "リストが見つかりません"
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:123
#, elixir-format
@@ -480,7 +480,7 @@ msgstr ""
#: lib/pleroma/web/oauth/oauth_controller.ex:321
#, elixir-format
msgid "Password reset is required"
-msgstr ""
+msgstr "パスワードのリセットが必要です"
#: lib/pleroma/tests/auth_test_controller.ex:9
#: lib/pleroma/web/activity_pub/activity_pub_controller.ex:6 lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:6
@@ -522,7 +522,7 @@ msgstr ""
#: lib/pleroma/plugs/ensure_authenticated_plug.ex:28
#, elixir-format
msgid "Two-factor authentication enabled, you must use a access token."
-msgstr ""
+msgstr "二段階認証が有効になっています。アクセストークンが必要です。"
#: lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex:210
#, elixir-format
@@ -552,7 +552,7 @@ msgstr ""
#: lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex:61
#, elixir-format
msgid "Web push subscription is disabled on this Pleroma instance"
-msgstr ""
+msgstr "このPleromaインスタンスではプッシュ通知が利用できません"
#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:451
#, elixir-format
@@ -562,19 +562,19 @@ msgstr ""
#: lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex:126
#, elixir-format
msgid "authorization required for timeline view"
-msgstr ""
+msgstr "タイムラインを閲覧するには認証が必要です"
#: lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex:24
#, elixir-format
msgid "Access denied"
-msgstr ""
+msgstr "アクセスが拒否されました"
#: lib/pleroma/web/mastodon_api/controllers/account_controller.ex:282
#, elixir-format
msgid "This API requires an authenticated user"
-msgstr ""
+msgstr "このAPIを利用するには認証が必要です"
#: lib/pleroma/plugs/user_is_admin_plug.ex:21
#, elixir-format
msgid "User is not an admin."
-msgstr ""
+msgstr "ユーザーは管理者ではありません。"
diff --git a/priv/repo/migrations/20220203224011_create_rules.exs b/priv/repo/migrations/20220203224011_create_rules.exs
@@ -0,0 +1,12 @@
+defmodule Pleroma.Repo.Migrations.CreateRules do
+ use Ecto.Migration
+
+ def change do
+ create_if_not_exists table(:rules) do
+ add(:priority, :integer, default: 0, null: false)
+ add(:text, :text, null: false)
+
+ timestamps()
+ end
+ end
+end
diff --git a/priv/repo/migrations/20240207035927_create_rich_media_card.exs b/priv/repo/migrations/20240207035927_create_rich_media_card.exs
@@ -0,0 +1,14 @@
+defmodule Pleroma.Repo.Migrations.CreateRichMediaCard do
+ use Ecto.Migration
+
+ def change do
+ create table(:rich_media_card) do
+ add(:url_hash, :bytea)
+ add(:fields, :map)
+
+ timestamps()
+ end
+
+ create(unique_index(:rich_media_card, [:url_hash]))
+ end
+end
diff --git a/priv/repo/migrations/20240223165000_create_bookmark_folders.exs b/priv/repo/migrations/20240223165000_create_bookmark_folders.exs
@@ -0,0 +1,27 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Repo.Migrations.CreateBookmarkFolders do
+ use Ecto.Migration
+
+ def change do
+ create_if_not_exists table(:bookmark_folders, primary_key: false) do
+ add(:id, :uuid, primary_key: true)
+ add(:name, :string, null: false)
+ add(:emoji, :string)
+ add(:user_id, references(:users, type: :uuid, on_delete: :delete_all))
+
+ timestamps()
+ end
+
+ alter table(:bookmarks) do
+ add_if_not_exists(
+ :folder_id,
+ references(:bookmark_folders, type: :uuid, on_delete: :nilify_all)
+ )
+ end
+
+ create_if_not_exists(unique_index(:bookmark_folders, [:user_id, :name]))
+ end
+end
diff --git a/priv/repo/migrations/20240406000000_add_hint_to_rules.exs b/priv/repo/migrations/20240406000000_add_hint_to_rules.exs
@@ -0,0 +1,13 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Repo.Migrations.AddHintToRules do
+ use Ecto.Migration
+
+ def change do
+ alter table(:rules) do
+ add_if_not_exists(:hint, :text)
+ end
+ end
+end
diff --git a/priv/static/schemas/litepub-0.1.jsonld b/priv/static/schemas/litepub-0.1.jsonld
@@ -2,6 +2,7 @@
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
+ "https://purl.archive.org/socialweb/webfinger",
{
"Emoji": "toot:Emoji",
"Hashtag": "as:Hashtag",
@@ -40,7 +41,9 @@
"@type": "@id"
},
"vcard": "http://www.w3.org/2006/vcard/ns#",
- "formerRepresentations": "litepub:formerRepresentations"
+ "formerRepresentations": "litepub:formerRepresentations",
+ "sm": "http://smithereen.software/ns#",
+ "nonAnonymous": "sm:nonAnonymous"
}
]
}
diff --git a/test/fixtures/ccworld-ap-bridge_note.json b/test/fixtures/ccworld-ap-bridge_note.json
@@ -0,0 +1 @@
+{"@context":"https://www.w3.org/ns/activitystreams","type":"Note","id":"https://cc.mkdir.uk/ap/note/e5d1d0a1-1ab3-4498-9949-588e3fdea286","attributedTo":"https://cc.mkdir.uk/ap/acct/hiira","inReplyTo":"","quoteUrl":"","content":"おはコンー","published":"2024-01-19T22:08:05Z","to":["https://www.w3.org/ns/activitystreams#Public"],"tag":null,"attachment":[],"object":null}
diff --git a/test/fixtures/minds-invalid-mention-post.json b/test/fixtures/minds-invalid-mention-post.json
@@ -0,0 +1 @@
+{"@context":"https://www.w3.org/ns/activitystreams","type":"Note","id":"https://www.minds.com/api/activitypub/users/1198929502760083472/entities/urn:comment:1600926863310458883:0:0:0:1600932467852709903","attributedTo":"https://www.minds.com/api/activitypub/users/1198929502760083472","content":"\u003Ca class=\u0022u-url mention\u0022 href=\u0022https://www.minds.com/lain\u0022 target=\u0022_blank\u0022\u003E@lain\u003C/a\u003E corn syrup.","to":["https://www.w3.org/ns/activitystreams#Public"],"cc":["https://www.minds.com/api/activitypub/users/1198929502760083472/followers","https://lain.com/users/lain"],"tag":[{"type":"Mention","href":"https://www.minds.com/api/activitypub/users/464237775479123984","name":"@lain"}],"url":"https://www.minds.com/newsfeed/1600926863310458883?focusedCommentUrn=urn:comment:1600926863310458883:0:0:0:1600932467852709903","published":"2024-02-04T17:34:03+00:00","inReplyTo":"https://lain.com/objects/36254095-c839-4167-bcc2-b361d5de9198","source":{"content":"@lain corn syrup.","mediaType":"text/plain"}}
+\ No newline at end of file
diff --git a/test/fixtures/minds-pleroma-mentioned-post.json b/test/fixtures/minds-pleroma-mentioned-post.json
@@ -0,0 +1 @@
+{"@context":["https://www.w3.org/ns/activitystreams","https://lain.com/schemas/litepub-0.1.jsonld",{"@language":"und"}],"actor":"https://lain.com/users/lain","attachment":[],"attributedTo":"https://lain.com/users/lain","cc":["https://lain.com/users/lain/followers"],"content":"which diet is the best for cognitive dissonance","context":"https://lain.com/contexts/98c8a130-e813-4797-8973-600e80114317","conversation":"https://lain.com/contexts/98c8a130-e813-4797-8973-600e80114317","id":"https://lain.com/objects/36254095-c839-4167-bcc2-b361d5de9198","published":"2024-02-04T17:11:23.931890Z","repliesCount":11,"sensitive":null,"source":{"content":"which diet is the best for cognitive dissonance","mediaType":"text/plain"},"summary":"","tag":[],"to":["https://www.w3.org/ns/activitystreams#Public"],"type":"Note"}
+\ No newline at end of file
diff --git a/test/fixtures/rich_media/google.html b/test/fixtures/rich_media/google.html
@@ -0,0 +1,12 @@
+<meta property="og:url" content="https://google.com">
+<meta property="og:type" content="website">
+<meta property="og:title" content="Google">
+<meta property="og:description" content="Search the world's information, including webpages, images, videos and more. Google has many special features to help you find exactly what you're looking for.">
+<meta property="og:image" content="">
+
+<meta name="twitter:card" content="summary_large_image">
+<meta property="twitter:domain" content="google.com">
+<meta property="twitter:url" content="https://google.com">
+<meta name="twitter:title" content="Google">
+<meta name="twitter:description" content="Search the world's information, including webpages, images, videos and more. Google has many special features to help you find exactly what you're looking for.">
+<meta name="twitter:image" content="">
diff --git a/test/fixtures/rich_media/oembed.html b/test/fixtures/rich_media/oembed.html
@@ -1,3 +1,3 @@
<link rel="alternate" type="application/json+oembed"
- href="http://example.com/oembed.json"
+ href="https://example.com/oembed.json"
title="Bacon Lollys oEmbed Profile" />
diff --git a/test/fixtures/rich_media/reddit.html b/test/fixtures/rich_media/reddit.html
@@ -0,0 +1,392 @@
+<!doctype html><html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en"><head><title>Twitter/X is getting weirder; where now for security news and analysis? : cybersecurity</title><meta name="keywords" content=" reddit, reddit.com, vote, comment, submit " /><meta name="description" content="I primarily use Twitter/X as a glorified RSS feed of security news and analysis reporting. Lately its getting heavier on ads and weird pushed..." /><meta name="referrer" content="always"><meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /><link type="application/opensearchdescription+xml" rel="search" href="/static/opensearch.xml"/><link rel="canonical" href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" /><link rel="amphtml" href="https://amp.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" /><meta name="viewport" content="width=1024"><link rel="shorturl" href="https://redd.it/16nf2ev"/><link rel="dns-prefetch" href="//out.reddit.com"><link rel="preconnect" href="//out.reddit.com"><meta property="og:image" content="https://www.redditstatic.com/new-icon.png"><meta property="og:ttl" content="600"><meta property="og:site_name" content="reddit"><meta property="og:description" content="I primarily use Twitter/X as a glorified RSS feed of security news and analysis reporting. Lately its getting heavier on ads and weird pushed..."><meta property="og:title" content="Twitter/X is getting weirder; where now for security news and analysis?"><meta property="al:android:package" content="com.reddit.frontpage"><meta property="al:ios:app_name" content="Reddit"><meta property="al:ios:url" content="reddit://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/"><meta property="al:ios:app_store_id" content="1064216828"><meta property="twitter:site" content="@reddit"><meta property="twitter:card" content="summary"><meta property="twitter:title" content="Twitter/X is getting weirder; where now for security news and..."><link rel="apple-touch-icon" sizes="57x57" href="//www.redditstatic.com/desktop2x/img/favicon/apple-icon-57x57.png" /><link rel="apple-touch-icon" sizes="60x60" href="//www.redditstatic.com/desktop2x/img/favicon/apple-icon-60x60.png" /><link rel="apple-touch-icon" sizes="72x72" href="//www.redditstatic.com/desktop2x/img/favicon/apple-icon-72x72.png" /><link rel="apple-touch-icon" sizes="76x76" href="//www.redditstatic.com/desktop2x/img/favicon/apple-icon-76x76.png" /><link rel="apple-touch-icon" sizes="114x114" href="//www.redditstatic.com/desktop2x/img/favicon/apple-icon-114x114.png" /><link rel="apple-touch-icon" sizes="120x120" href="//www.redditstatic.com/desktop2x/img/favicon/apple-icon-120x120.png" /><link rel="apple-touch-icon" sizes="144x144" href="//www.redditstatic.com/desktop2x/img/favicon/apple-icon-144x144.png" /><link rel="apple-touch-icon" sizes="152x152" href="//www.redditstatic.com/desktop2x/img/favicon/apple-icon-152x152.png" /><link rel="apple-touch-icon" sizes="180x180" href="//www.redditstatic.com/desktop2x/img/favicon/apple-icon-180x180.png" /><link rel="icon" type="image/png" sizes="192x192" href="//www.redditstatic.com/desktop2x/img/favicon/android-icon-192x192.png" /><link rel="icon" type="image/png" sizes="32x32" href="//www.redditstatic.com/desktop2x/img/favicon/favicon-32x32.png" /><link rel="icon" type="image/png" sizes="96x96" href="//www.redditstatic.com/desktop2x/img/favicon/favicon-96x96.png" /><link rel="icon" type="image/png" sizes="16x16" href="//www.redditstatic.com/desktop2x/img/favicon/favicon-16x16.png" /><link rel="manifest" href="//www.redditstatic.com/desktop2x/img/favicon/manifest.json"/><meta name="msapplication-TileColor" content="#ffffff"/><meta name="msapplication-TileImage" content="//www.redditstatic.com/desktop2x/img/favicon/ms-icon-144x144.png"/><meta name="theme-color" content="#ffffff"/><link rel="alternate" type="application/atom+xml" title="RSS" href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/.rss" /><link rel="stylesheet" type="text/css" href="//www.redditstatic.com/reddit.YXox_dqXzrc.css" media="all"><link rel="stylesheet" type="text/css" href="//www.redditstatic.com/expando.gMzRK16vwrQ.css" media="all"><link rel="stylesheet" type="text/css" href="//www.redditstatic.com/crosspost-preview.De3P20Yb4PY.css" media="all"><link rel="stylesheet" type="text/css" href="//www.redditstatic.com/author-tooltip.1VKQhhDIRMI.css" media="all"><link rel="stylesheet" type="text/css" href="//www.redditstatic.com/listing-comments.AZZO7Kj_O88.css" media="all"><link rel="stylesheet" type="text/css" href="//www.redditstatic.com/popup-notification.6-JvPBpHWMo.css" media="all"><link rel="stylesheet" type="text/css" href="//www.redditstatic.com/about-this-ad-modal.zVecmeeCuWY.css" media="all"><link rel="stylesheet" type="text/css" href="//www.redditstatic.com/desktoponboarding.k2RNrAG42v4.css" media="all"><link rel="stylesheet" type="text/css" href="//www.redditstatic.com/videoplayer.ANmi3DZjWG4.css" media="all"><link rel="stylesheet" type="text/css" href="//www.redditstatic.com/videoplayercontrols.a_TwaTy76-k.css" media="all"><!--[if gte IE 8]><!--><link rel="stylesheet" href="https://b.thumbs.redditmedia.com/ba_fm376ctS_mGlZGabqddmkhth3jqnccUyhKW7iGBo.css" ref="applied_subreddit_stylesheet" title="applied_subreddit_stylesheet" type="text/css"><!--<![endif]--><!--[if gte IE 9]><!--><script type="text/javascript" src="//www.redditstatic.com/reddit-init.en.a6Ar54Z0rBo.js"></script><!--<![endif]--><!--[if lt IE 9]><script type="text/javascript" src="//www.redditstatic.com/reddit-init-legacy.en.sAcI0xGFcwg.js"></script><![endif]--><script type="text/javascript" src="//www.redditstatic.com/videoplayer.XCrwE8Bi5A4.js"></script><script type="text/javascript" id="config">r.setup({"ajax_domain": "www.reddit.com", "post_site": "cybersecurity", "gold": false, "scraped_image_extensions": ["gif", "jpeg", "jpg", "png", "tiff"], "poisoning_report_mac": null, "requires_eu_cookie_policy": false, "nsfw_media_acknowledged": false, "stats_domain": "", "feature_net_neutrality": false, "cur_screen_name": "", "country_code": "", "facebook_app_id": "322647334569188", "loid": "000000000uhuuh3cqp", "is_sponsor": false, "has_gold_subscription": false, "feature_author_tooltip_users": true, "user_id": false, "pref_email_messages": false, "poisoning_canary": null, "logged": false, "over_18": false, "show_archived_signup_cta": true, "loid_created": 1708312402583, "mweb_blacklist_expressions": ["^/prefs/?", "^/live/?", "/message/compose", "/m/", "^/subreddits/create", "^/gold", "^/advertising", "^/promoted", "^/buttons"], "feature_noreferrer_to_noopener": true, "is_posts_mod": false, "modhash": "", "external_frame": false, "feature_cookie_consent_banner": true, "send_logs": true, "user_subscription_size": 0, "listing_over_18": false, "https_endpoint": "https://www.reddit.com", "extension": null, "embedded": false, "event_target": {"target_id": 2578913527, "target_type": "self", "target_sort": "confidence", "target_fullname": "t3_16nf2ev"}, "use_onetrust": false, "ads_loading_timeout_ms": 5000, "enabled_experiments": {}, "cache_policy": "loggedout_www", "admin_message_acct": "/r/reddit.com", "hidden_post_card_enabled": false, "advertiser_category": null, "events_collector_v2_url": "https://www.reddit.com/api/vote", "debug": false, "has_subscribed": false, "static_root": "//www.redditstatic.com", "server_time": 1708312403.0, "feature_no_subscription_step": "control_1", "feature_swap_steps_two_and_three_recalibration": "control_2", "pref_no_profanity": true, "share_tracking_ts": 1708312403232, "cur_domain": "reddit.com", "browser_supports_d2x": false, "events_collector_url": "https://www.reddit.com/api/vote", "embed_preview_url": "https://rebed.redditmedia.com/embed", "gild_url": "/framedGild", "cur_link": "t3_16nf2ev", "user_in_timeout": false, "share_tracking_hmac": null, "live_orangereds_pref": true, "feature_blocked_user_enabled": false, "ad_serving_events_sample_rate": "1.00", "is_fake": false, "renderstyle": "html", "framed_modal_url": "/framedModal/", "feature_adblock_v2_events_enabled": false, "user_age": false, "vote_hash": "4piNiL72kb0vN4fiRASdIAWORVe57zU0rsH8oo7AucKTKbAJQLfbHHo5RHL3/TKQx1yuwQbqBTpkfx1e/jhijKl6RZROrrFN0hJLyzXBCO0hF9IabySW12sBeNpMhTkrRffaXEgdyVj6LxzXZ2XAHPZPz5b7yVHbKOdgXhJ2ENI=", "link_websocket_url": "", "events_collector_secret": "Ua3epahc7ZiengeeVaeG6eingahke7", "pref_video_autoplay": true, "events_collector_key": "RedditFrontend3", "allow_nonessential_cookies": false, "scraped_domains": ["gfycat.com", "imgur.com"], "expando_preference": "subreddit_default", "store_visits": false, "onetrust_client_id": "14003311-a669-490b-a682-56294eb02bf2", "cur_site": "t5_2u559", "new_window": false, "pref_beta": false, "channels_mod_permissions_enabled": true, "eu_cookie_max_attempts": 3, "pageInfo": {"actionName": "front.GET_comments", "statsVerification": "388228f7328f4f4378f87784f3f47137498db25f", "type": "post_detail", "verification": "388228f7328f4f4378f87784f3f47137498db25f", "statsName": "front.GET_comments"}, "user_websocket_url": null, "signature_header": "X-Signature", "media_domain": "www.redditmedia.com", "whitelist_status": "all_ads", "signature_header_v2": "X-Signature-v2", "cur_listing": "cybersecurity", "email_verified": false, "status_msg": {"fetching": "fetching title...", "loading": "loading...", "submitting": "submitting..."}, "link_limit": 200, "stats_sample_rate": "0", "loid_version": 2, "eu_cookie": "eu_cookie", "is_moderator_somewhere": false, "d2x_domain": "https://new.reddit.com/"})</script><style type="text/css">/* Custom css: use this block to insert special translation-dependent css in the page header */</style></head><body class="single-page comments-page" ><script>var frame = document.createElement('iframe'); frame.style.display = 'none'; frame.referrer = 'no-referrer'; frame.id = 'gtm-jail'; frame.name = JSON.stringify({ subreddit: r.config.post_site, origin: location.origin, url: location.href, userMatching: false, userId: r.config.user_id, advertiserCategory: r.config.advertiser_category, adsStatus: r.config.whitelist_status, }); frame.src = '//' + "www.redditmedia.com" + '/gtm/jail?cb=' + "8CqR7FcToPI"; document.body.appendChild(frame);</script><div id="header" role="banner"><a tabindex="1" href="#content" id="jumpToContent">jump to content</a><div id="sr-header-area"><div class="width-clip"><div class="dropdown srdrop" onclick="open_menu(this)"><span class="selected title">my subreddits</span></div><div class="drop-choices srdrop"><a href="https://www.reddit.com/subreddits/" class="bottom-option choice" >edit subscriptions</a></div><div class="sr-list"><ul class="flat-list sr-bar hover" ><li ><a href="https://www.reddit.com/r/popular/" class="choice" >popular</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/all/" class="choice" >all</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/random/" class="random choice" >random</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/users/" class="choice" >users</a></li></ul><span class="separator"> | </span><ul class="flat-list sr-bar hover" id='sr-bar'><li ><a href="https://www.reddit.com/r/AskReddit/" class="choice" >AskReddit</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/pics/" class="choice" >pics</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/funny/" class="choice" >funny</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/movies/" class="choice" >movies</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/gaming/" class="choice" >gaming</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/worldnews/" class="choice" >worldnews</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/news/" class="choice" >news</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/todayilearned/" class="choice" >todayilearned</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/nottheonion/" class="choice" >nottheonion</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/explainlikeimfive/" class="choice" >explainlikeimfive</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/mildlyinteresting/" class="choice" >mildlyinteresting</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/DIY/" class="choice" >DIY</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/videos/" class="choice" >videos</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/OldSchoolCool/" class="choice" >OldSchoolCool</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/TwoXChromosomes/" class="choice" >TwoXChromosomes</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/tifu/" class="choice" >tifu</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/Music/" class="choice" >Music</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/books/" class="choice" >books</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/LifeProTips/" class="choice" >LifeProTips</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/dataisbeautiful/" class="choice" >dataisbeautiful</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/aww/" class="choice" >aww</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/science/" class="choice" >science</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/space/" class="choice" >space</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/Showerthoughts/" class="choice" >Showerthoughts</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/askscience/" class="choice" >askscience</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/Jokes/" class="choice" >Jokes</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/IAmA/" class="choice" >IAmA</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/Futurology/" class="choice" >Futurology</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/sports/" class="choice" >sports</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/UpliftingNews/" class="choice" >UpliftingNews</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/food/" class="choice" >food</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/nosleep/" class="choice" >nosleep</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/creepy/" class="choice" >creepy</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/history/" class="choice" >history</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/gifs/" class="choice" >gifs</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/InternetIsBeautiful/" class="choice" >InternetIsBeautiful</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/GetMotivated/" class="choice" >GetMotivated</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/gadgets/" class="choice" >gadgets</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/announcements/" class="choice" >announcements</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/WritingPrompts/" class="choice" >WritingPrompts</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/philosophy/" class="choice" >philosophy</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/Documentaries/" class="choice" >Documentaries</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/EarthPorn/" class="choice" >EarthPorn</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/photoshopbattles/" class="choice" >photoshopbattles</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/listentothis/" class="choice" >listentothis</a></li><li ><span class="separator">-</span><a href="https://www.reddit.com/r/blog/" class="choice" >blog</a></li></ul></div><a href="https://www.reddit.com/subreddits/" id="sr-more-link" >more »</a></div></div><div id="header-bottom-left"><a href="/" id="header-img" class="default-header" title="">reddit.com</a> <span class="hover pagename redditname"><a href="https://www.reddit.com/r/cybersecurity/" >cybersecurity</a></span><ul class="tabmenu " ><li class='selected'><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" class="choice" >comments</a></li></ul></div><div id="header-bottom-right"><span class="user">Want to join? <a href="https://www.reddit.com/login" class="login-required login-link" >Log in</a> or <a href="https://www.reddit.com/login" class="login-required" >sign up</a> in seconds.</span><span class="separator">|</span><ul class="flat-list hover" ><li ><a href="javascript:void(0)" class="pref-lang choice" onclick="return showlang();" >English</a></li></ul></div></div><div class="side"><div class='spacer'><form action="https://www.reddit.com/r/cybersecurity/search" id="search" role="search"><input type="text" name="q" placeholder="search" tabindex="20"><input type="submit" value="" tabindex="22"><div id="searchexpando" class="infobar"><label><input type="checkbox" name="restrict_sr" tabindex="21">limit my search to r/cybersecurity</label><div id="moresearchinfo"><p>use the following search parameters to narrow your results:</p><dl><dt>subreddit:<i>subreddit</i></dt><dd>find submissions in "subreddit"</dd><dt>author:<i>username</i></dt><dd>find submissions by "username"</dd><dt>site:<i>example.com</i></dt><dd>find submissions from "example.com"</dd><dt>url:<i>text</i></dt><dd>search for "text" in url</dd><dt>selftext:<i>text</i></dt><dd>search for "text" in self post contents</dd><dt>self:yes (or self:no)</dt><dd>include (or exclude) self posts</dd><dt>nsfw:yes (or nsfw:no)</dt><dd>include (or exclude) results marked as NSFW</dd></dl><p>e.g. <code>subreddit:aww site:imgur.com dog</code></p><p><a href="https://www.reddit.com/wiki/search">see the search faq for details.</a></p></div><p><a href="https://www.reddit.com/wiki/search" id="search_showmore">advanced search: by author, subreddit...</a></p></div></form></div><div class='spacer'><div class="linkinfo"><div class="date"><span>this post was submitted on  </span><time datetime="2023-09-20T07:34:36+00:00">20 Sep 2023</time></div><div class="score"><span class="number">241</span> <span class="word">points</span> (92% upvoted)</div><div class="shortlink">shortlink:  <input type="text" value="https://redd.it/16nf2ev" readonly="readonly" id="shortlink-text" /></div></div></div><div class='spacer'><form method="post" action="https://www.reddit.com/r/cybersecurity/post/login" id="login_login-main" class="login-form login-form-side"><input type="hidden" name="op" value="login-main" /><input name="user" placeholder="username" type="text" maxlength="20" tabindex="1"/><input name="passwd" placeholder="password" type="password" tabindex="1"/><div class="g-recaptcha" data-sitekey="6LeTnxkTAAAAAN9QEuDZRpn90WwKk_R1TRW_g-JC"></div><div class="status"></div><div id="remember-me"><input type="checkbox" name="rem" id="rem-login-main" tabindex="1" /><label for="rem-login-main">remember me</label><a class="recover-password" href="/password">reset password</a></div><div class="submit"><span class="throbber"></span><button class="btn" type="submit" tabindex="1">login</button></div><div class="clear"></div></form></div><div class='spacer'></div><div class='spacer'><div class="sidebox submit submit-link"><div class="morelink"><a href="https://www.reddit.com/r/cybersecurity/submit" data-event-action="submit" data-type="subreddit" data-event-detail="link" class="login-required access-required" target="_top" >Submit a new link</a><div class="nub"></div></div></div></div><div class='spacer'><div class="sidebox submit submit-text"><div class="morelink"><a href="https://www.reddit.com/r/cybersecurity/submit?selftext=true" data-event-action="submit" data-type="subreddit" data-event-detail="self" class="login-required access-required" target="_top" >Submit a new text post</a><div class="nub"></div></div></div></div><div class='spacer'><a href="/premium" alt="get premium" class="premium-banner-outer"><form action="/premium" class="premium-banner"><div class="premium-banner__logo"></div><div class="premium-banner__title">Get an ad-free experience with special benefits, and directly support Reddit.</div><button class="premium-banner__button">get reddit premium</button></form></a></div><div class='spacer'><div class="titlebox"><h1 class="hover redditname"><a href="https://www.reddit.com/r/cybersecurity/" class="hover" >cybersecurity</a></h1><span class="fancy-toggle-button subscribe-button toggle" style="" data-sr_name="cybersecurity" ><a class="option active add login-required" href="#" tabindex="100" >join</a><a class="option remove" href="#">leave</a></span><span class="subscribers"><span class="number">688,076</span> <span class="word">readers</span></span><p class="users-online" title="users viewing this subreddit in the past 15 minutes"><span class="number">592</span> <span class="word">users here now</span></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t5_2u559nsa"><input type="hidden" name="thing_id" value="t5_2u559"/><div class="usertext-body may-blank-within md-container " ><div class="md"><h1><strong>NOTICE:</strong></h1>
+
+<h1><strong>This sidebar and rules are no longer being updated. To see the current sidebar and rules you must view them on new reddit.</strong></h1>
+
+<p><a href="https://new.reddit.com/r/cybersecurity">https://new.reddit.com/r/cybersecurity</a></p>
+
+<p>This security forum is oriented towards private white hat security professionals. Please note, the 'old' Reddit is no longer kept up to date. Please use 'new' Reddit. </p>
+
+<ol>
+<li><p><strong>No Low Effort / Poor Quality Posts</strong>
+"This security forum is oriented towards private white hat security professionals." If a post has very basic information, it is not appropriate for this sub. For example, "why passwords are important" is too fundamental.</p></li>
+<li><p><strong>Must be relevant to security professionals</strong>
+This is not a general security subreddit. Posts related to burglar alarms, weapons, and similar concepts are not appropriate for this sub.</p></li>
+<li><p><strong>No fundamental security questions or tech support requests</strong>
+Basic questions on security concepts and fundamentals and requests for tech support are not appropriate for this subreddit.</p></li>
+<li><p><strong>SECURITY FIRST (no editorializing)</strong>
+This is the guiding principle for all posts. No editorializing and no political agendas. Posts discussing political issues that affect security are fine, but the post must be geared towards the security implication. Such posts will be heavily monitored and comments may be locked as needed.</p></li>
+<li><p><strong>Civility</strong>
+We're all professionals. Be excellent to each other.</p></li>
+<li><p><strong>No Advertising</strong>
+Want to share information or resources? Message The Mods to find out how! You would rather build a relationship with the <a href="/r/CyberSecurity">/r/CyberSecurity</a> community than get banned! Please message the mods before posting links to your own projects or if you have any questions about the advertising policies</p></li>
+<li><p><strong>No Personally-Identifiable Information</strong>
+Do not post personally-identifiable information, unless the source has consented to it.</p></li>
+</ol>
+
+<hr/>
+
+<p><strong>This subreddit is oriented towards computer security professionals</strong></p>
+
+<p>Need help with a computer security problem? </p>
+
+<ul>
+<li><p><a href="/r/cybersecurity_help">/r/cybersecurity_help</a></p></li>
+<li><p><a href="/r/cybersecurity101">/r/cybersecurity101</a></p></li>
+</ul>
+
+<p>Are you looking for a job or looking to hire someone?</p>
+
+<ul>
+<li><a href="/r/cybersecurityjobs">/r/cybersecurityjobs</a> (currently closed)</li>
+</ul>
+
+<p>Are you looking for home defense and security systems (alarms, CCTV, ect)? </p>
+
+<ul>
+<li><a href="/r/Locksmith">/r/Locksmith</a></li>
+<li><a href="/r/homedefense">/r/homedefense</a></li>
+<li><a href="/r/homeautomation">/r/homeautomation</a></li>
+<li><a href="/r/videosurveillance">/r/videosurveillance</a></li>
+<li><a href="/r/physec/">/r/physec/</a></li>
+</ul>
+
+<p>Are you a security guard or physical security professional? </p>
+
+<ul>
+<li><a href="/r/ProtectAndServe">/r/ProtectAndServe</a></li>
+<li><a href="/r/Secguards">/r/Secguards</a> </li>
+</ul>
+
+<p>Are you here to post an advertisement or spam? </p>
+
+<ul>
+<li><a href="/r/spam">/r/spam</a></li>
+</ul>
+
+<p>Other Security Communities:</p>
+
+<ul>
+<li><a href="/r/ComputerSecurity">/r/ComputerSecurity</a></li>
+<li><a href="/r/crypto">/r/crypto</a> </li>
+<li><a href="/r/hacking">/r/hacking</a> </li>
+<li><a href="/r/netdef">/r/netdef</a><br/></li>
+<li><a href="/r/intelligence">/r/intelligence</a></li>
+<li><a href="/r/opsec">/r/opsec</a></li>
+<li><a href="/r/OperationsSecurity">/r/OperationsSecurity</a></li>
+<li><a href="/r/privacy">/r/privacy</a> </li>
+<li><a href="/r/redteam">/r/redteam</a></li>
+</ul>
+
+<p>Just for fun:</p>
+
+<ul>
+<li><a href="/r/scambaiting">/r/scambaiting</a></li>
+</ul>
+
+<hr/>
+
+<hr/>
+
+<p>We ask all users with a potential conflict of interest (e.g. security product manufacturers and service providers) to disclose their affiliation. This allows subscribers to ask them questions about their areas of expertise while ensuring transparency. For questions about this status, to request a user flair, or if you think that these users have violated this subreddit's policies, please <a href="http://www.reddit.com/message/compose?to=%2Fr%2Fcybersecurity">message the mods</a>.</p>
+</div>
+</div></form><div class="bottom"><span class="age">a community for <time title="Tue May 22 05:07:59 2012 UTC" datetime="2012-05-22T05:07:59+00:00">11 years</time></span></div></div></div><div class='spacer'></div><div class='spacer'><div class="sidecontentbox " ><div class="title"><h1>MODERATORS</h1></div><ul class="content"><li class="message-button centered"><a class="c-btn c-btn-primary login-required" href="/message/compose/?to=/r/cybersecurity">message the mods</a></li></ul></div></div><div class='spacer'><div class="read-next-container"><aside class="read-next"><header class="read-next-header"><div class="read-next-header-title">discussions in <a href="https://www.reddit.com/r/cybersecurity/?ref=readnext" >r/cybersecurity</a></div><nav class="read-next-nav read-next-nav-left"><span class="read-next-button prev"><</span><span class="read-next-button next">></span></nav><div class="read-next-nav read-next-nav-right"><span class="read-next-dismiss">X</span></div></header><div class="read-next-list"><div class="listing read-next-listing"><div class="listing-contents"><a id="read-next-link-t3_1au4n18" class="read-next-link may-blank" href="/r/cybersecurity/comments/1au4n18/what_are_some_musthave_rules_and_policies_that/?ref=readnext"><div class="read-next-meta"><span class="score" title="106">106</span>  · <span class="comments">45 comments</span> </div><div class="read-next-title">What are some "must-have" rules and policies that you configure on every firewall you worked with?</div></a><a id="read-next-link-t3_1au7265" class="read-next-link may-blank" href="/r/cybersecurity/comments/1au7265/has_it_been_anyone_elses_experience_when_meeting/?ref=readnext"><div class="read-next-meta"><span class="score" title="55">55</span>  · <span class="comments">27 comments</span> </div><div class="read-next-title">Has it been anyone else's experience when meeting people in the field and networking that they all got their positions because they knew someone even with zero experience?</div></a><a id="read-next-link-t3_1aty4yv" class="read-next-link may-blank" href="/r/cybersecurity/comments/1aty4yv/when_it_comes_to_cyber_threats_india_not_the_us/?ref=readnext"><div class="read-next-meta"><span class="score" title="116">116</span>  · <span class="comments">17 comments</span> </div><div class="read-next-thumbnail"><img src="//b.thumbs.redditmedia.com/fN4jxU3yZomnnLik7d9diWdRnTkbl67V4YjF0oXfSfY.jpg" width='70' height='36' alt=""></div><div class="read-next-title">When it comes to cyber threats, India – not the US – is China’s biggest concern</div></a><a id="read-next-link-t3_1atmron" class="read-next-link may-blank" href="/r/cybersecurity/comments/1atmron/gpt4_can_hack_websites_with_733_success_rate_in/?ref=readnext"><div class="read-next-meta"><span class="score" title="449">449</span>  · <span class="comments">57 comments</span> </div><div class="read-next-thumbnail"><img src="//a.thumbs.redditmedia.com/grGQ4Xa2kcPcM6pqkNIDlUWrOa_1Q98cv7xt9EOyyg8.jpg" width='70' height='36' alt=""></div><div class="read-next-title">GPT4 can hack websites with 73.3% success rate in sandboxed environment</div></a><a id="read-next-link-t3_1aubpd0" class="read-next-link may-blank" href="/r/cybersecurity/comments/1aubpd0/bouncing_back_from_a_cyber_attack/?ref=readnext"><div class="read-next-meta"></div><div class="read-next-thumbnail"><img src="//b.thumbs.redditmedia.com/Jefbctam7l-lzDPxyKfR_L5oOV68nxzrg7E89LrTfpQ.jpg" width='70' height='23' alt=""></div><div class="read-next-title">Bouncing back from a cyber attack</div></a><a id="read-next-link-t3_1au92zu" class="read-next-link may-blank" href="/r/cybersecurity/comments/1au92zu/mentorship_monday_post_all_career_education_and/?ref=readnext"><div class="read-next-meta"><span class="score" title="5">5</span>  · <span class="comments">8 comments</span> </div><div class="read-next-title">Mentorship Monday - Post All Career, Education and Job questions here!</div></a><a id="read-next-link-t3_1atjev1" class="read-next-link may-blank" href="/r/cybersecurity/comments/1atjev1/what_is_a_security_feature_that_is_really/?ref=readnext"><div class="read-next-meta"><span class="score" title="192">192</span>  · <span class="comments">296 comments</span> </div><div class="read-next-title">What is a security feature that is really "security theater"?</div></a><a id="read-next-link-t3_1atltdb" class="read-next-link may-blank" href="/r/cybersecurity/comments/1atltdb/resources_to_learn_threat_hunting/?ref=readnext"><div class="read-next-meta"><span class="score" title="94">94</span>  · <span class="comments">17 comments</span> </div><div class="read-next-title">Resources to learn Threat Hunting?</div></a><a id="read-next-link-t3_1atuoz8" class="read-next-link may-blank" href="/r/cybersecurity/comments/1atuoz8/seeking_cybersecurity_awareness_materials_for/?ref=readnext"><div class="read-next-meta"><span class="score" title="15">15</span>  · <span class="comments">9 comments</span> </div><div class="read-next-title">Seeking Cybersecurity Awareness Materials for School Teenagers age group students: Any Recommendations?</div></a><a id="read-next-link-t3_1au1z7n" class="read-next-link may-blank" href="/r/cybersecurity/comments/1au1z7n/fbis_mostwanted_zeus_and_icedid_malware/?ref=readnext"><div class="read-next-meta"><span class="score" title="4">4</span>  · <span class="comments">1 comment</span> </div><div class="read-next-thumbnail"><img src="//b.thumbs.redditmedia.com/sKOGKVqS9KxTIcMVdCAsY2em1FWOai0QXCxUcxjoW7U.jpg" width='70' height='36' alt=""></div><div class="read-next-title">FBI's Most-Wanted Zeus and IcedID Malware Mastermind Pleads Guilty</div></a></div></div></div></aside></div></div></div><a name="content"></a><div class="content" role="main" ><section class="infobar listingsignupbar"><a href="/login" class="login-required listingsignupbar__container"><h2 class="listingsignupbar__title">Welcome to Reddit,</h2><p class="listingsignupbar__desc">the front page of the internet.</p><div class="listingsignupbar__cta-container"><span class="c-btn c-btn-primary c-pull-left listingsignupbar__cta-button">Become a Redditor</span><p class="listingsignupbar__cta-desc">and join one of thousands of communities.</p></div></a><a href="#" class="listingsignupbar__close" title="close">×</a></section><style>body >.content .link .rank, .rank-spacer { width: 1.1ex } body >.content .link .midcol, .midcol-spacer { width: 4.1ex } .adsense-wrap { background-color: #eff7ff; font-size: 18px; padding-left: 5.2ex; padding-right: 5px; }</style><div id="siteTable" class="sitetable linklisting"><div class=" thing id-t3_16nf2ev linkflair odd  link self" id="thing_t3_16nf2ev" onclick="click_thing(this)" data-fullname="t3_16nf2ev" data-type="link" data-gildings="0" data-whitelist-status="all_ads" data-is-gallery="false" data-author="skeedooshski" data-author-fullname="t2_2w3lp8yl" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-timestamp="1695195276000" data-url="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-domain="self.cybersecurity" data-rank="" data-comments-count="98" data-score="241" data-promoted="false" data-nsfw="false" data-spoiler="false" data-oc="false" data-num-crossposts="0" data-context="comments" ><p class="parent"></p><span class="rank"></span><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="score dislikes" title="240">240</div><div class="score unvoted" title="241">241</div><div class="score likes" title="242">242</div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><a class="thumbnail invisible-when-pinned self may-blank " data-event-action="thumbnail" href="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" ></a><div class="entry unvoted"><div class="top-matter"><p class="title"><a class="title may-blank " data-event-action="title" href="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" tabindex="1" >Twitter/X is getting weirder; where now for security news and analysis?</a><span class="flairrichtext flaircolordark linkflairlabel " title="News - General" style="background-color: #0079d3; border-color: #0079d3;"><span>News - General</span></span> <span class="domain">(<a href="/r/cybersecurity/">self.cybersecurity</a>)</span></p><p class="tagline ">submitted <time title="Wed Sep 20 07:34:36 2023 UTC" datetime="2023-09-20T07:34:36+00:00" class="live-timestamp">5 months ago</time> by <a href="https://www.reddit.com/user/skeedooshski" class="author may-blank id-t2_2w3lp8yl" >skeedooshski</a><span class="userattrs"></span></p></div><div class="expando expando-uninitialized" data-pin-condition="function() {return this.style.display != 'none';}" ><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t3_16nf2evdgw"><input type="hidden" name="thing_id" value="t3_16nf2ev"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>I primarily use Twitter/X as a glorified RSS feed of security news and analysis reporting. Lately its getting heavier on ads and weird pushed content. </p>
+
+<p>I'm not for replacing it with 5 different platforms, but if I was going to use something else, what are peeps using?</p>
+</div>
+</div></form></div><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-event-action="comments" class="bylink comments may-blank" rel="nofollow" >98 comments</a></li><li class="share"><a class="post-sharing-button" href="javascript: void 0;">share</a></li><li class="link-save-button save-button login-required"><a href="#">save</a></li><li><form action="/post/hide" method="post" class="state-button hide-button"><input type="hidden" name="executed" value="hidden" /><span><a href="javascript:void(0)" class=" " data-event-action="hide" onclick="change_state(this, 'hide', hide_thing);">hide</a></span></form></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li></ul><div class="reportform report-t3_16nf2ev"></div></div><div class="child" ></div><div class="clearleft"></div></div><div class="clearleft"></div></div><div class='commentarea'><div class="panestack-title"><span class="title">all 98 comments</span></div><div class="menuarea"><div class="spacer"><span class="dropdown-title lightdrop">sorted by: </span><div class="dropdown lightdrop" onclick="open_menu(this)"><span class="selected">best</span></div><div class="drop-choices lightdrop"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/?sort=top" class="choice" >top</a><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/?sort=new" class="choice" >new</a><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/?sort=controversial" class="choice" >controversial</a><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/?sort=old" class="choice" >old</a><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/?sort=random" class="hidden choice" >random</a><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/?sort=qa" class="choice" >q&a</a><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/?sort=live" class="hidden choice" >live (beta)</a></div></div><div class="spacer"></div></div><section class="infobar commentsignupbar"><div class="commentsignupbar__container"><a href="/login" class="login-required commentsignupbar__link-wrapper"><textarea class="commentsignupbar__textarea"></textarea><div class="commentsignupbar__textarea-above"><h2 class="commentsignupbar__title">Want to add to the discussion?</h2><p class="commentsignupbar__desc">Post a comment!</p><div class="commentsignupbar__cta-container"><span class="c-btn c-btn-primary commentsignupbar__cta-button">Create an account</span></div></div></a></div></section><div id="siteTable_t3_16nf2ev" class="sitetable nestedlisting"><div class=" thing id-t1_k1f2su1 noncollapsed   comment " id="thing_t1_k1f2su1" onclick="click_thing(this)" data-fullname="t1_k1f2su1" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="goretsky" data-author-fullname="t2_3tihk" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f2su1/" ><p class="parent"><a name="k1f2su1"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/goretsky" class="author may-blank id-t2_3tihk" >goretsky</a><span class="flairrichtext flaircolordark flair " title="Aryeh Goretsky" style="background-color: #edeff1; border-color: #edeff1;"><span>Aryeh Goretsky</span></span><span class="userattrs"></span> <span class="score dislikes" title="163">163 points</span><span class="score unvoted" title="164">164 points</span><span class="score likes" title="165">165 points</span> <time title="Wed Sep 20 14:11:13 2023 UTC" datetime="2023-09-20T14:11:13+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(5 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1f2su1eco"><input type="hidden" name="thing_id" value="t1_k1f2su1"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Hello,</p>
+
+<p>I use Reddit for this purpose. I have created several security themed multireddits for the purpose of tracking security-related topics:</p>
+
+<p><a href="https://old.reddit.com/user/goretsky/m/security/">https://old.reddit.com/user/goretsky/m/security/</a> - tracks about 90 active security-related subreddits, but no vendors or open source projects and is regularly pruned of inactive subreddits.</p>
+
+<p><a href="https://old.reddit.com/user/goretsky/m/security_vendor/">https://old.reddit.com/user/goretsky/m/security_vendor/</a> - tracks about three dozen security vendor and open source project subreddits. </p>
+
+<p><a href="https://old.reddit.com/user/goretsky/m/security_inactive/">https://old.reddit.com/user/goretsky/m/security_inactive/</a> - a multireddit specifically for subreddits that were in the first two, but no longer seem to be active (lets me periodically check them for activity) </p>
+
+<p>You can view these by new, hot, top and so forth to get ideas of what's current, might be an emerging issue, what was historically significant, and so forth.</p>
+
+<p>Regards,</p>
+
+<p>Aryeh Goretsky</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f2su1/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f2su1/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1f2su1"></div></div><div class="child"><div id="siteTable_t1_k1f2su1" class="sitetable listing"><div class=" thing id-t1_k1gtm9x noncollapsed   comment " id="thing_t1_k1gtm9x" onclick="click_thing(this)" data-fullname="t1_k1gtm9x" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="cccanterbury" data-author-fullname="t2_93f0t46r4" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1gtm9x/" ><p class="parent"><a name="k1gtm9x"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/cccanterbury" class="author may-blank id-t2_93f0t46r4" >cccanterbury</a><span class="userattrs"></span> <span class="score dislikes" title="7">7 points</span><span class="score unvoted" title="8">8 points</span><span class="score likes" title="9">9 points</span> <time title="Wed Sep 20 20:17:18 2023 UTC" datetime="2023-09-20T20:17:18+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1gtm9x948"><input type="hidden" name="thing_id" value="t1_k1gtm9x"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Wow thanks Aryeh!</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1gtm9x/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1gtm9x/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1f2su1" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1gtm9x"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1hmfej noncollapsed   comment " id="thing_t1_k1hmfej" onclick="click_thing(this)" data-fullname="t1_k1hmfej" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="kitwillybb" data-author-fullname="t2_fdvexzhhd" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1hmfej/" ><p class="parent"><a name="k1hmfej"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/kitwillybb" class="author may-blank id-t2_fdvexzhhd" >kitwillybb</a><span class="userattrs"></span> <span class="score dislikes" title="2">2 points</span><span class="score unvoted" title="3">3 points</span><span class="score likes" title="4">4 points</span> <time title="Wed Sep 20 23:15:13 2023 UTC" datetime="2023-09-20T23:15:13+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1hmfejqzw"><input type="hidden" name="thing_id" value="t1_k1hmfej"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Thanks for this.</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1hmfej/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1hmfej/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1f2su1" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1hmfej"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1i7bji noncollapsed   comment " id="thing_t1_k1i7bji" onclick="click_thing(this)" data-fullname="t1_k1i7bji" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="Robbbbbbbbb" data-author-fullname="t2_a6lsi" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1i7bji/" ><p class="parent"><a name="k1i7bji"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/Robbbbbbbbb" class="author may-blank id-t2_a6lsi" >Robbbbbbbbb</a><span class="userattrs"></span> <span class="score dislikes" title="2">2 points</span><span class="score unvoted" title="3">3 points</span><span class="score likes" title="4">4 points</span> <time title="Thu Sep 21 01:39:52 2023 UTC" datetime="2023-09-21T01:39:52+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1i7bjic0u"><input type="hidden" name="thing_id" value="t1_k1i7bji"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Thanks! Great multireddits</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1i7bji/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1i7bji/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1f2su1" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1i7bji"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1mafik noncollapsed   comment " id="thing_t1_k1mafik" onclick="click_thing(this)" data-fullname="t1_k1mafik" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="That_CatDad" data-author-fullname="t2_4d8ihz1f" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1mafik/" ><p class="parent"><a name="k1mafik"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/That_CatDad" class="author may-blank id-t2_4d8ihz1f" >That_CatDad</a><span class="userattrs"></span> <span class="score dislikes" title="2">2 points</span><span class="score unvoted" title="3">3 points</span><span class="score likes" title="4">4 points</span> <time title="Thu Sep 21 20:49:59 2023 UTC" datetime="2023-09-21T20:49:59+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1mafikfpn"><input type="hidden" name="thing_id" value="t1_k1mafik"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Well I just discovered multireddits, thank you so much this will definitely change how I use this site</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1mafik/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1mafik/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1f2su1" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1mafik"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing noncollapsed   deleted comment " onclick="click_thing(this)" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1ehfna/" ><p class="parent"></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><em>[deleted]</em> <time title="Wed Sep 20 11:30:36 2023 UTC" datetime="2023-09-20T11:30:36+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(6 children)</a></p><div class="usertext grayed"><input type="hidden" name="thing_id" value=""/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>[deleted]</p>
+</div>
+</div></div><ul class="flat-list buttons"></ul><div class="reportform report-t1_k1ehfna"></div></div><div class="child"><div id="siteTable_deleted" class="sitetable listing"><div class=" thing id-t1_k1gmnbd noncollapsed   comment " id="thing_t1_k1gmnbd" onclick="click_thing(this)" data-fullname="t1_k1gmnbd" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="uncannysalt" data-author-fullname="t2_3wpcoju1" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1gmnbd/" ><p class="parent"><a name="k1gmnbd"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/uncannysalt" class="author may-blank id-t2_3wpcoju1" >uncannysalt</a><span class="flairrichtext flaircolorlight flair " title="Security Architect" style="background-color: #373c3f; border-color: #373c3f;"><span>Security Architect</span></span><span class="userattrs"></span> <span class="score dislikes" title="12">12 points</span><span class="score unvoted" title="13">13 points</span><span class="score likes" title="14">14 points</span> <time title="Wed Sep 20 19:38:55 2023 UTC" datetime="2023-09-20T19:38:55+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1gmnbdxe8"><input type="hidden" name="thing_id" value="t1_k1gmnbd"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Exactly. Most prolific security folks have feeds available.</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1gmnbd/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1gmnbd/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1gmnbd"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1gobsy noncollapsed   comment " id="thing_t1_k1gobsy" onclick="click_thing(this)" data-fullname="t1_k1gobsy" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="th4ntis" data-author-fullname="t2_5i6unrry" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1gobsy/" ><p class="parent"><a name="k1gobsy"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/th4ntis" class="author may-blank id-t2_5i6unrry" >th4ntis</a><span class="userattrs"></span> <span class="score dislikes" title="5">5 points</span><span class="score unvoted" title="6">6 points</span><span class="score likes" title="7">7 points</span> <time title="Wed Sep 20 19:48:06 2023 UTC" datetime="2023-09-20T19:48:06+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(4 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1gobsytgn"><input type="hidden" name="thing_id" value="t1_k1gobsy"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>I would actually love to do this but need to figure out how. I haven't looked into it yet but this is on my list of things to do.<br/>
+Any recommendations or tips would be helpful.</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1gobsy/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1gobsy/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1gobsy"></div></div><div class="child"><div id="siteTable_t1_k1gobsy" class="sitetable listing"><div class=" thing noncollapsed   deleted comment " onclick="click_thing(this)" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1grrni/" ><p class="parent"></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><em>[deleted]</em> <time title="Wed Sep 20 20:07:09 2023 UTC" datetime="2023-09-20T20:07:09+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(2 children)</a></p><div class="usertext grayed"><input type="hidden" name="thing_id" value=""/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>[deleted]</p>
+</div>
+</div></div><ul class="flat-list buttons"></ul><div class="reportform report-t1_k1grrni"></div></div><div class="child"><div id="siteTable_deleted" class="sitetable listing"><div class=" thing id-t1_k1mbol3 noncollapsed   comment " id="thing_t1_k1mbol3" onclick="click_thing(this)" data-fullname="t1_k1mbol3" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="th4ntis" data-author-fullname="t2_5i6unrry" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1mbol3/" ><p class="parent"><a name="k1mbol3"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/th4ntis" class="author may-blank id-t2_5i6unrry" >th4ntis</a><span class="userattrs"></span> <span class="score dislikes" title="0">0 points</span><span class="score unvoted" title="1">1 point</span><span class="score likes" title="2">2 points</span> <time title="Thu Sep 21 20:57:08 2023 UTC" datetime="2023-09-21T20:57:08+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(1 child)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1mbol3bgn"><input type="hidden" name="thing_id" value="t1_k1mbol3"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>For someone getting started, any RSS links you recommend?</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1mbol3/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1mbol3/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1mbol3"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1k9di0 noncollapsed   comment " id="thing_t1_k1k9di0" onclick="click_thing(this)" data-fullname="t1_k1k9di0" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="Bleord" data-author-fullname="t2_6qov61wx" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1k9di0/" ><p class="parent"><a name="k1k9di0"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/Bleord" class="author may-blank id-t2_6qov61wx" >Bleord</a><span class="userattrs"></span> <span class="score dislikes" title="0">0 points</span><span class="score unvoted" title="1">1 point</span><span class="score likes" title="2">2 points</span> <time title="Thu Sep 21 13:40:03 2023 UTC" datetime="2023-09-21T13:40:03+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1k9di0ftw"><input type="hidden" name="thing_id" value="t1_k1k9di0"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Lots of web browsers have it built in.</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1k9di0/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1k9di0/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1gobsy" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1k9di0"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1eutsw noncollapsed   comment " id="thing_t1_k1eutsw" onclick="click_thing(this)" data-fullname="t1_k1eutsw" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="beagle_bathouse" data-author-fullname="t2_83rlaeff" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1eutsw/" ><p class="parent"><a name="k1eutsw"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/beagle_bathouse" class="author may-blank id-t2_83rlaeff" >beagle_bathouse</a><span class="userattrs"></span> <span class="score dislikes" title="59">59 points</span><span class="score unvoted" title="60">60 points</span><span class="score likes" title="61">61 points</span> <time title="Wed Sep 20 13:17:09 2023 UTC" datetime="2023-09-20T13:17:09+00:00" class="live-timestamp">5 months ago</time><time class="edited-timestamp" title="last edited 9 days ago" datetime="2024-02-09T18:57:48+00:00">*</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(8 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1eutswfzs"><input type="hidden" name="thing_id" value="t1_k1eutsw"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>desert person pet quiet worm hurry absorbed like cause rainstorm</p>
+
+<p><em>This post was mass deleted and anonymized with <a href="https://redact.dev">Redact</a></em></p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1eutsw/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1eutsw/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1eutsw"></div></div><div class="child"><div id="siteTable_t1_k1eutsw" class="sitetable listing"><div class=" thing id-t1_k1fe1am noncollapsed   comment " id="thing_t1_k1fe1am" onclick="click_thing(this)" data-fullname="t1_k1fe1am" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="volume_two" data-author-fullname="t2_dmeg9cabu" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fe1am/" ><p class="parent"><a name="k1fe1am"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/volume_two" class="author may-blank id-t2_dmeg9cabu" >volume_two</a><span class="userattrs"></span> <span class="score dislikes" title="12">12 points</span><span class="score unvoted" title="13">13 points</span><span class="score likes" title="14">14 points</span> <time title="Wed Sep 20 15:20:53 2023 UTC" datetime="2023-09-20T15:20:53+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(2 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1fe1am0rj"><input type="hidden" name="thing_id" value="t1_k1fe1am"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Nazis have free speech rights too! And the Constitution guarantees their freedom of speech on X.</p>
+
+<p>You non-Nazis are the nazis!</p>
+
+<h1>/S</h1>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fe1am/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fe1am/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1eutsw" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1fe1am"></div></div><div class="child"><div id="siteTable_t1_k1fe1am" class="sitetable listing"><div class=" thing id-t1_k1ffalw noncollapsed   comment " id="thing_t1_k1ffalw" onclick="click_thing(this)" data-fullname="t1_k1ffalw" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="beagle_bathouse" data-author-fullname="t2_83rlaeff" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1ffalw/" ><p class="parent"><a name="k1ffalw"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/beagle_bathouse" class="author may-blank id-t2_83rlaeff" >beagle_bathouse</a><span class="userattrs"></span> <span class="score dislikes" title="9">9 points</span><span class="score unvoted" title="10">10 points</span><span class="score likes" title="11">11 points</span> <time title="Wed Sep 20 15:28:26 2023 UTC" datetime="2023-09-20T15:28:26+00:00" class="live-timestamp">5 months ago</time><time class="edited-timestamp" title="last edited 9 days ago" datetime="2024-02-09T18:57:43+00:00">*</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(1 child)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1ffalwqyw"><input type="hidden" name="thing_id" value="t1_k1ffalw"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>wakeful mighty steer fragile quickest rinse weather saw melodic boast</p>
+
+<p><em>This post was mass deleted and anonymized with <a href="https://redact.dev">Redact</a></em></p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1ffalw/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1ffalw/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1fe1am" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1ffalw"></div></div><div class="child"><div id="siteTable_t1_k1ffalw" class="sitetable listing"><div class=" thing id-t1_k1fgnqb noncollapsed   comment " id="thing_t1_k1fgnqb" onclick="click_thing(this)" data-fullname="t1_k1fgnqb" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="volume_two" data-author-fullname="t2_dmeg9cabu" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fgnqb/" ><p class="parent"><a name="k1fgnqb"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/volume_two" class="author may-blank id-t2_dmeg9cabu" >volume_two</a><span class="userattrs"></span> <span class="score dislikes" title="-1">-1 points</span><span class="score unvoted" title="0">0 points</span><span class="score likes" title="1">1 point</span> <time title="Wed Sep 20 15:36:36 2023 UTC" datetime="2023-09-20T15:36:36+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1fgnqbj60"><input type="hidden" name="thing_id" value="t1_k1fgnqb"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Somebody give this person an upvote!</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fgnqb/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fgnqb/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1ffalw" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1fgnqb"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1fx6xl collapsed collapsed-for-reason   comment " id="thing_t1_k1fx6xl" onclick="click_thing(this)" data-fullname="t1_k1fx6xl" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="stacksmasher" data-author-fullname="t2_4ceji" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fx6xl/" ><p class="parent"><a name="k1fx6xl"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[+]</a><a href="https://www.reddit.com/user/stacksmasher" class="author may-blank id-t2_4ceji" >stacksmasher</a><span class="userattrs"></span> <span class="collapsed-reason">comment score below threshold</span><span class="score dislikes" title="-8">-8 points</span><span class="score unvoted" title="-7">-7 points</span><span class="score likes" title="-6">-6 points</span> <time title="Wed Sep 20 17:13:37 2023 UTC" datetime="2023-09-20T17:13:37+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(4 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1fx6xluja"><input type="hidden" name="thing_id" value="t1_k1fx6xl"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Infosec.exchange is horrible with very little engagement</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fx6xl/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fx6xl/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1eutsw" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1fx6xl"></div></div><div class="child"><div id="siteTable_t1_k1fx6xl" class="sitetable listing"><div class=" thing id-t1_k1hdzxo noncollapsed   comment " id="thing_t1_k1hdzxo" onclick="click_thing(this)" data-fullname="t1_k1hdzxo" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="thomasareed" data-author-fullname="t2_ogy3b" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1hdzxo/" ><p class="parent"><a name="k1hdzxo"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/thomasareed" class="author may-blank id-t2_ogy3b" >thomasareed</a><span class="userattrs"></span> <span class="score dislikes" title="5">5 points</span><span class="score unvoted" title="6">6 points</span><span class="score likes" title="7">7 points</span> <time title="Wed Sep 20 22:18:19 2023 UTC" datetime="2023-09-20T22:18:19+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(1 child)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1hdzxo59d"><input type="hidden" name="thing_id" value="t1_k1hdzxo"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Hard disagree. What you get out of it depends on what you put in. If you’re just looking to curate a feed of infosec news, this may not be it. If you’re looking for a group of friends to have interesting discussions with, both infosec-related and not, you can make it that place. But that happens slowly and with participation.</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1hdzxo/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1hdzxo/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1fx6xl" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1hdzxo"></div></div><div class="child"><div id="siteTable_t1_k1hdzxo" class="sitetable listing"><div class=" thing id-t1_k1hh7ow noncollapsed   comment " id="thing_t1_k1hh7ow" onclick="click_thing(this)" data-fullname="t1_k1hh7ow" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="stacksmasher" data-author-fullname="t2_4ceji" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1hh7ow/" ><p class="parent"><a name="k1hh7ow"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/stacksmasher" class="author may-blank id-t2_4ceji" >stacksmasher</a><span class="userattrs"></span> <span class="score dislikes" title="-3">-3 points</span><span class="score unvoted" title="-2">-2 points</span><span class="score likes" title="-1">-1 points</span> <time title="Wed Sep 20 22:39:33 2023 UTC" datetime="2023-09-20T22:39:33+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1hh7owe2s"><input type="hidden" name="thing_id" value="t1_k1hh7ow"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Nope I only use it for data. You know most critical issues are announced on Twitter?</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1hh7ow/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1hh7ow/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1hdzxo" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1hh7ow"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1fyt2y noncollapsed   comment " id="thing_t1_k1fyt2y" onclick="click_thing(this)" data-fullname="t1_k1fyt2y" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="beagle_bathouse" data-author-fullname="t2_83rlaeff" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fyt2y/" ><p class="parent"><a name="k1fyt2y"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/beagle_bathouse" class="author may-blank id-t2_83rlaeff" >beagle_bathouse</a><span class="userattrs"></span> <span class="score dislikes" title="5">5 points</span><span class="score unvoted" title="6">6 points</span><span class="score likes" title="7">7 points</span> <time title="Wed Sep 20 17:22:55 2023 UTC" datetime="2023-09-20T17:22:55+00:00" class="live-timestamp">5 months ago</time><time class="edited-timestamp" title="last edited 9 days ago" datetime="2024-02-09T18:57:28+00:00">*</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(1 child)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1fyt2yioo"><input type="hidden" name="thing_id" value="t1_k1fyt2y"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>erect narrow wipe plant tub nail rain shame angle unpack</p>
+
+<p><em>This post was mass deleted and anonymized with <a href="https://redact.dev">Redact</a></em></p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fyt2y/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fyt2y/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1fx6xl" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1fyt2y"></div></div><div class="child"><div id="siteTable_t1_k1fyt2y" class="sitetable listing"><div class=" thing id-t1_k1gafku noncollapsed   comment " id="thing_t1_k1gafku" onclick="click_thing(this)" data-fullname="t1_k1gafku" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="stacksmasher" data-author-fullname="t2_4ceji" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1gafku/" ><p class="parent"><a name="k1gafku"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/stacksmasher" class="author may-blank id-t2_4ceji" >stacksmasher</a><span class="userattrs"></span> <span class="score dislikes" title="-3">-3 points</span><span class="score unvoted" title="-2">-2 points</span><span class="score likes" title="-1">-1 points</span> <time title="Wed Sep 20 18:29:55 2023 UTC" datetime="2023-09-20T18:29:55+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1gafkuw1o"><input type="hidden" name="thing_id" value="t1_k1gafku"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Agree 1000000000% but the exchange platform is not it</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1gafku/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1gafku/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1fyt2y" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1gafku"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1ed9ru noncollapsed   comment " id="thing_t1_k1ed9ru" onclick="click_thing(this)" data-fullname="t1_k1ed9ru" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="sonicoak" data-author-fullname="t2_40lq8qtkt" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1ed9ru/" ><p class="parent"><a name="k1ed9ru"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/sonicoak" class="author may-blank id-t2_40lq8qtkt" >sonicoak</a><span class="flairrichtext flaircolorlight flair " title="Governance, Risk, & Compliance" style="background-color: #373c3f; border-color: #373c3f;"><span>Governance, Risk, & Compliance</span></span><span class="userattrs"></span> <span class="score dislikes" title="96">96 points</span><span class="score unvoted" title="97">97 points</span><span class="score likes" title="98">98 points</span> <time title="Wed Sep 20 10:49:11 2023 UTC" datetime="2023-09-20T10:49:11+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(19 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1ed9rulb2"><input type="hidden" name="thing_id" value="t1_k1ed9ru"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p><a href="https://infosec.exchange/home">https://infosec.exchange/home</a> , it is a Mastodon server.</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1ed9ru/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1ed9ru/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1ed9ru"></div></div><div class="child"><div id="siteTable_t1_k1ed9ru" class="sitetable listing"><div class=" thing id-t1_k1f2191 noncollapsed   comment " id="thing_t1_k1f2191" onclick="click_thing(this)" data-fullname="t1_k1f2191" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="Chrishamilton2007" data-author-fullname="t2_5goj3" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f2191/" ><p class="parent"><a name="k1f2191"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/Chrishamilton2007" class="author may-blank id-t2_5goj3" >Chrishamilton2007</a><span class="userattrs"></span> <span class="score dislikes" title="80">80 points</span><span class="score unvoted" title="81">81 points</span><span class="score likes" title="82">82 points</span> <time title="Wed Sep 20 14:06:15 2023 UTC" datetime="2023-09-20T14:06:15+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(4 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1f21918rv"><input type="hidden" name="thing_id" value="t1_k1f2191"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>I dunno, I'm sure Jerry Bell is cool and all but I'm hesitant to point people to what could essentially turn into a private facebook group overnight.</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f2191/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f2191/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1ed9ru" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1f2191"></div></div><div class="child"><div id="siteTable_t1_k1f2191" class="sitetable listing"><div class=" thing id-t1_k1fckr4 noncollapsed   comment " id="thing_t1_k1fckr4" onclick="click_thing(this)" data-fullname="t1_k1fckr4" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="Versed_Percepton" data-author-fullname="t2_94cm2jqqy" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fckr4/" ><p class="parent"><a name="k1fckr4"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/Versed_Percepton" class="author may-blank id-t2_94cm2jqqy" >Versed_Percepton</a><span class="userattrs"></span> <span class="score dislikes" title="20">20 points</span><span class="score unvoted" title="21">21 points</span><span class="score likes" title="22">22 points</span> <time title="Wed Sep 20 15:12:00 2023 UTC" datetime="2023-09-20T15:12:00+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(1 child)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1fckr4d58"><input type="hidden" name="thing_id" value="t1_k1fckr4"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Jerry has <a href="https://Infosec.exchange">Infosec.exchange</a> open and very rarely are there admin issues at the federation level. Also, I have NEVER seen Jerry pull a 'shitty admin move' in the couple years I have been on the instance. Let me tell you, there were times I wish he would. But he is just not that type of person.</p>
+
+<p>He has been working his ass off to expand the instance to support the influx of new users, he is very open about this in his daily feeds too. So, there is no way in hell the instance will become 'private' with all the hard work he has put in here.</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fckr4/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fckr4/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1f2191" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1fckr4"></div></div><div class="child"><div id="siteTable_t1_k1fckr4" class="sitetable listing"><div class=" thing id-t1_k1flqhj noncollapsed   comment " id="thing_t1_k1flqhj" onclick="click_thing(this)" data-fullname="t1_k1flqhj" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="moker" data-author-fullname="t2_31fvu" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1flqhj/" ><p class="parent"><a name="k1flqhj"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/moker" class="author may-blank id-t2_31fvu" >moker</a><span class="userattrs"></span> <span class="score dislikes" title="4">4 points</span><span class="score unvoted" title="5">5 points</span><span class="score likes" title="6">6 points</span> <time title="Wed Sep 20 16:06:31 2023 UTC" datetime="2023-09-20T16:06:31+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1flqhjact"><input type="hidden" name="thing_id" value="t1_k1flqhj"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>thank you :)</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1flqhj/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1flqhj/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1fckr4" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1flqhj"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1f3k7m noncollapsed   comment " id="thing_t1_k1f3k7m" onclick="click_thing(this)" data-fullname="t1_k1f3k7m" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="Gangrif" data-author-fullname="t2_13q213" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f3k7m/" ><p class="parent"><a name="k1f3k7m"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/Gangrif" class="author may-blank id-t2_13q213" >Gangrif</a><span class="userattrs"></span> <span class="score dislikes" title="16">16 points</span><span class="score unvoted" title="17">17 points</span><span class="score likes" title="18">18 points</span> <time title="Wed Sep 20 14:16:09 2023 UTC" datetime="2023-09-20T14:16:09+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1f3k7m4bq"><input type="hidden" name="thing_id" value="t1_k1f3k7m"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>for what it’s worth. jerry has been tirelessly administering infosec.exchange for years. and doesn’t seem likely to stop. and if he does…. you can move easily to another instance. defcon runs one, the mastodon folks run one. i run one (though mine is mainly for me.) and they all federate with the others. so you don’t miss out being on you’re own or one other than your friends are on.</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f3k7m/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f3k7m/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1f2191" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1f3k7m"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1h3ys0 noncollapsed   comment " id="thing_t1_k1h3ys0" onclick="click_thing(this)" data-fullname="t1_k1h3ys0" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="moker" data-author-fullname="t2_31fvu" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1h3ys0/" ><p class="parent"><a name="k1h3ys0"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/moker" class="author may-blank id-t2_31fvu" >moker</a><span class="userattrs"></span> <span class="score dislikes" title="7">7 points</span><span class="score unvoted" title="8">8 points</span><span class="score likes" title="9">9 points</span> <time title="Wed Sep 20 21:17:06 2023 UTC" datetime="2023-09-20T21:17:06+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1h3ys0lx7"><input type="hidden" name="thing_id" value="t1_k1h3ys0"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>yes. After 7 years of being a free and open forum, I have decided to make <a href="https://infosec.exchange">infosec.exchange</a> a private facebook group. I had been on the fence about it until now.</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1h3ys0/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1h3ys0/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1f2191" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1h3ys0"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1expm6 noncollapsed   comment " id="thing_t1_k1expm6" onclick="click_thing(this)" data-fullname="t1_k1expm6" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="movement2012" data-author-fullname="t2_tuqrm" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1expm6/" ><p class="parent"><a name="k1expm6"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/movement2012" class="author may-blank id-t2_tuqrm" >movement2012</a><span class="userattrs"></span> <span class="score dislikes" title="17">17 points</span><span class="score unvoted" title="18">18 points</span><span class="score likes" title="19">19 points</span> <time title="Wed Sep 20 13:37:17 2023 UTC" datetime="2023-09-20T13:37:17+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(6 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1expm616a"><input type="hidden" name="thing_id" value="t1_k1expm6"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Mastodon feels a bit dry. Are there too few people, or am I not following enough?</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1expm6/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1expm6/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1ed9ru" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1expm6"></div></div><div class="child"><div id="siteTable_t1_k1expm6" class="sitetable listing"><div class=" thing noncollapsed   deleted comment " onclick="click_thing(this)" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f2t7m/" ><p class="parent"></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><em>[deleted]</em> <time title="Wed Sep 20 14:11:17 2023 UTC" datetime="2023-09-20T14:11:17+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(1 child)</a></p><div class="usertext grayed"><input type="hidden" name="thing_id" value=""/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>[deleted]</p>
+</div>
+</div></div><ul class="flat-list buttons"></ul><div class="reportform report-t1_k1f2t7m"></div></div><div class="child"><div id="siteTable_deleted" class="sitetable listing"><div class=" thing id-t1_k1f4kto noncollapsed   comment " id="thing_t1_k1f4kto" onclick="click_thing(this)" data-fullname="t1_k1f4kto" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="Myrion_Phoenix" data-author-fullname="t2_zisex" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f4kto/" ><p class="parent"><a name="k1f4kto"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/Myrion_Phoenix" class="author may-blank id-t2_zisex" >Myrion_Phoenix</a><span class="userattrs"></span> <span class="score dislikes" title="7">7 points</span><span class="score unvoted" title="8">8 points</span><span class="score likes" title="9">9 points</span> <time title="Wed Sep 20 14:22:37 2023 UTC" datetime="2023-09-20T14:22:37+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1f4ktohzv"><input type="hidden" name="thing_id" value="t1_k1f4kto"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>It's also helpful to follow some hashtags (which the Mastodon android app can't, but f.ex. Tusky can and the web interface also works).</p>
+
+<p>I follow #fido2 and #cryptography, for example, as well as stuff like #bookstodon.</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f4kto/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f4kto/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1f4kto"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1fvt58 noncollapsed   comment " id="thing_t1_k1fvt58" onclick="click_thing(this)" data-fullname="t1_k1fvt58" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="vitriolix" data-author-fullname="t2_38fl0" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fvt58/" ><p class="parent"><a name="k1fvt58"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/vitriolix" class="author may-blank id-t2_38fl0" >vitriolix</a><span class="userattrs"></span> <span class="score dislikes" title="14">14 points</span><span class="score unvoted" title="15">15 points</span><span class="score likes" title="16">16 points</span> <time title="Wed Sep 20 17:05:34 2023 UTC" datetime="2023-09-20T17:05:34+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1fvt58i8r"><input type="hidden" name="thing_id" value="t1_k1fvt58"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>checkout the local feeds on a few instances to get lots of good content and find people to follow:</p>
+
+<p><a href="https://infosec.exchange/public/local">https://infosec.exchange/public/local</a></p>
+
+<p><a href="https://fosstodon.org/public/local">https://fosstodon.org/public/local</a></p>
+
+<p><a href="https://hachyderm.io/public/local">https://hachyderm.io/public/local</a></p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fvt58/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fvt58/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1expm6" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1fvt58"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1fduhp noncollapsed   comment " id="thing_t1_k1fduhp" onclick="click_thing(this)" data-fullname="t1_k1fduhp" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fduhp/" ><p class="parent"><a name="k1fduhp"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><span>[deleted]</span> <span class="score dislikes" title="2">2 points</span><span class="score unvoted" title="3">3 points</span><span class="score likes" title="4">4 points</span> <time title="Wed Sep 20 15:19:46 2023 UTC" datetime="2023-09-20T15:19:46+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1fduhpwm0"><input type="hidden" name="thing_id" value="t1_k1fduhp"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p><a href="https://elk.zone/infosec.exchange/@fY54DtPKe6rxMF/110757471778047861" rel="nofollow">https://elk.zone/infosec.exchange/@fY54DtPKe6rxMF/110757471778047861</a></p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fduhp/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fduhp/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1expm6" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1fduhp"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1hfihx noncollapsed   comment " id="thing_t1_k1hfihx" onclick="click_thing(this)" data-fullname="t1_k1hfihx" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="mkosmo" data-author-fullname="t2_3asub" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1hfihx/" ><p class="parent"><a name="k1hfihx"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/mkosmo" class="author may-blank id-t2_3asub" >mkosmo</a><span class="flairrichtext flaircolorlight flair " title="Security Architect" style="background-color: #373c3f; border-color: #373c3f;"><span>Security Architect</span></span><span class="userattrs"></span> <span class="score dislikes" title="2">2 points</span><span class="score unvoted" title="3">3 points</span><span class="score likes" title="4">4 points</span> <time title="Wed Sep 20 22:28:12 2023 UTC" datetime="2023-09-20T22:28:12+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(1 child)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1hfihxyb6"><input type="hidden" name="thing_id" value="t1_k1hfihx"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>The fediverse isn't nearly as populated as it's zealots would lead you to believe, unfortunately. Great concept, but just doesn't have the momentum.</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1hfihx/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1hfihx/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1expm6" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1hfihx"></div></div><div class="child"><div id="siteTable_t1_k1hfihx" class="sitetable listing"><div class=" thing id-t1_k1kb6wr noncollapsed   comment " id="thing_t1_k1kb6wr" onclick="click_thing(this)" data-fullname="t1_k1kb6wr" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="WollCel" data-author-fullname="t2_7q0snzpw" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1kb6wr/" ><p class="parent"><a name="k1kb6wr"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/WollCel" class="author may-blank id-t2_7q0snzpw" >WollCel</a><span class="userattrs"></span> <span class="score dislikes" title="1">1 point</span><span class="score unvoted" title="2">2 points</span><span class="score likes" title="3">3 points</span> <time title="Thu Sep 21 13:52:01 2023 UTC" datetime="2023-09-21T13:52:01+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1kb6wre6m"><input type="hidden" name="thing_id" value="t1_k1kb6wr"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Yeah it’s growing though, it doesn’t help how ideologically splintered instances can get</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1kb6wr/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1kb6wr/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1hfihx" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1kb6wr"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1f481h noncollapsed   comment " id="thing_t1_k1f481h" onclick="click_thing(this)" data-fullname="t1_k1f481h" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="Zncon" data-author-fullname="t2_hpvos" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f481h/" ><p class="parent"><a name="k1f481h"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/Zncon" class="author may-blank id-t2_hpvos" >Zncon</a><span class="userattrs"></span> <span class="score dislikes" title="9">9 points</span><span class="score unvoted" title="10">10 points</span><span class="score likes" title="11">11 points</span> <time title="Wed Sep 20 14:20:24 2023 UTC" datetime="2023-09-20T14:20:24+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1f481hrvj"><input type="hidden" name="thing_id" value="t1_k1f481h"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>The entire first page (and most of the rest of them) is politics and nonsense at the moment - not exactly an amazing recommendation. =/</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f481h/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f481h/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1ed9ru" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1f481h"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing noncollapsed   deleted comment " onclick="click_thing(this)" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fgax9/" ><p class="parent"></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><em>[deleted]</em> <time title="Wed Sep 20 15:34:29 2023 UTC" datetime="2023-09-20T15:34:29+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(1 child)</a></p><div class="usertext grayed"><input type="hidden" name="thing_id" value=""/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>[deleted]</p>
+</div>
+</div></div><ul class="flat-list buttons"></ul><div class="reportform report-t1_k1fgax9"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1envz7 collapsed collapsed-for-reason   comment " id="thing_t1_k1envz7" onclick="click_thing(this)" data-fullname="t1_k1envz7" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="profshmex" data-author-fullname="t2_6dja5aq1" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1envz7/" ><p class="parent"><a name="k1envz7"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[+]</a><a href="https://www.reddit.com/user/profshmex" class="author may-blank id-t2_6dja5aq1" >profshmex</a><span class="userattrs"></span> <span class="collapsed-reason">comment score below threshold</span><span class="score dislikes" title="-30">-30 points</span><span class="score unvoted" title="-29">-29 points</span><span class="score likes" title="-28">-28 points</span> <time title="Wed Sep 20 12:25:02 2023 UTC" datetime="2023-09-20T12:25:02+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(2 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1envz78qq"><input type="hidden" name="thing_id" value="t1_k1envz7"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Login creds? Nice try 😉</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1envz7/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1envz7/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1ed9ru" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1envz7"></div></div><div class="child"><div id="siteTable_t1_k1envz7" class="sitetable listing"><div class=" thing id-t1_k1f0mqa noncollapsed   comment " id="thing_t1_k1f0mqa" onclick="click_thing(this)" data-fullname="t1_k1f0mqa" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="SteveDinn" data-author-fullname="t2_n2ejp" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f0mqa/" ><p class="parent"><a name="k1f0mqa"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/SteveDinn" class="author may-blank id-t2_n2ejp" >SteveDinn</a><span class="userattrs"></span> <span class="score dislikes" title="0">0 points</span><span class="score unvoted" title="1">1 point</span><span class="score likes" title="2">2 points</span> <time title="Wed Sep 20 13:56:58 2023 UTC" datetime="2023-09-20T13:56:58+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1f0mqa639"><input type="hidden" name="thing_id" value="t1_k1f0mqa"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Try <a href="https://infosec.exchange/explore" rel="nofollow">https://infosec.exchange/explore</a> instead.</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f0mqa/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f0mqa/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1envz7" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1f0mqa"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1f0eas noncollapsed   comment " id="thing_t1_k1f0eas" onclick="click_thing(this)" data-fullname="t1_k1f0eas" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="moker" data-author-fullname="t2_31fvu" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f0eas/" ><p class="parent"><a name="k1f0eas"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/moker" class="author may-blank id-t2_31fvu" >moker</a><span class="userattrs"></span> <span class="score dislikes" title="49">49 points</span><span class="score unvoted" title="50">50 points</span><span class="score likes" title="51">51 points</span> <time title="Wed Sep 20 13:55:24 2023 UTC" datetime="2023-09-20T13:55:24+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(8 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1f0eas19s"><input type="hidden" name="thing_id" value="t1_k1f0eas"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>I run <a href="https://infosec.exchange">https://infosec.exchange</a> - it has about 17000 active members, and among several other security related instances.</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f0eas/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f0eas/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1f0eas"></div></div><div class="child"><div id="siteTable_t1_k1f0eas" class="sitetable listing"><div class=" thing id-t1_k1f8o48 noncollapsed   comment " id="thing_t1_k1f8o48" onclick="click_thing(this)" data-fullname="t1_k1f8o48" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="Elder_Meow_667" data-author-fullname="t2_ul0ujln5" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f8o48/" ><p class="parent"><a name="k1f8o48"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/Elder_Meow_667" class="author may-blank id-t2_ul0ujln5" >Elder_Meow_667</a><span class="userattrs"></span> <span class="score dislikes" title="3">3 points</span><span class="score unvoted" title="4">4 points</span><span class="score likes" title="5">5 points</span> <time title="Wed Sep 20 14:48:00 2023 UTC" datetime="2023-09-20T14:48:00+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1f8o48gnj"><input type="hidden" name="thing_id" value="t1_k1f8o48"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Jerrrrrry! Hehe</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f8o48/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f8o48/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1f0eas" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1f8o48"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1fsfsq noncollapsed   comment " id="thing_t1_k1fsfsq" onclick="click_thing(this)" data-fullname="t1_k1fsfsq" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="Popka_Akoola" data-author-fullname="t2_2ym6xry6" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fsfsq/" ><p class="parent"><a name="k1fsfsq"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/Popka_Akoola" class="author may-blank id-t2_2ym6xry6" >Popka_Akoola</a><span class="userattrs"></span> <span class="score dislikes" title="3">3 points</span><span class="score unvoted" title="4">4 points</span><span class="score likes" title="5">5 points</span> <time title="Wed Sep 20 16:45:58 2023 UTC" datetime="2023-09-20T16:45:58+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(1 child)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1fsfsqxhm"><input type="hidden" name="thing_id" value="t1_k1fsfsq"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>May get downvoted for this but having been one of the earlier adopters into mastodon and infosec exchange specifically, I just don't see how it's better than X. I get a lot of crazy things have happened, but 95% of posts I see on infosec exchange are people congratulating themselves and being so proud they left Twitter and the other 5% are people introducing themselves and talking about their day/treating the platform like Twitter.</p>
+
+<p>I love the idea of Mastodon in general and I have high hopes for it's future, but I really think people are deluding themselves if they say it has better content at the moment.</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fsfsq/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fsfsq/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1f0eas" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1fsfsq"></div></div><div class="child"><div id="siteTable_t1_k1fsfsq" class="sitetable listing"><div class=" thing id-t1_k1ibs3x noncollapsed   comment " id="thing_t1_k1ibs3x" onclick="click_thing(this)" data-fullname="t1_k1ibs3x" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="syn-ack-fin" data-author-fullname="t2_6nnp3" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1ibs3x/" ><p class="parent"><a name="k1ibs3x"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/syn-ack-fin" class="author may-blank id-t2_6nnp3" >syn-ack-fin</a><span class="userattrs"></span> <span class="score dislikes" title="4">4 points</span><span class="score unvoted" title="5">5 points</span><span class="score likes" title="6">6 points</span> <time title="Thu Sep 21 02:11:56 2023 UTC" datetime="2023-09-21T02:11:56+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1ibs3xlez"><input type="hidden" name="thing_id" value="t1_k1ibs3x"/><div class="usertext-body may-blank-within md-container " ><div class="md"><blockquote>
+<p>I just don’t see how it’s better than X</p>
+</blockquote>
+
+<p>Better is obviously relative, but Mastodon does take more work. The end result is that you have a feed solely with the information you want and not what is pushed on you. Oh and fewer nazis is nice too.</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1ibs3x/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1ibs3x/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1fsfsq" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1ibs3x"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1foxtg noncollapsed   controversial comment " id="thing_t1_k1foxtg" onclick="click_thing(this)" data-fullname="t1_k1foxtg" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="Fallingdamage" data-author-fullname="t2_44txb" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1foxtg/" ><p class="parent"><a name="k1foxtg"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/Fallingdamage" class="author may-blank id-t2_44txb" >Fallingdamage</a><span class="userattrs"></span> <span class="score dislikes" title="4">4 points</span><span class="score unvoted" title="5">5 points</span><span class="score likes" title="6">6 points</span> <time title="Wed Sep 20 16:25:25 2023 UTC" datetime="2023-09-20T16:25:25+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(3 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1foxtgp8a"><input type="hidden" name="thing_id" value="t1_k1foxtg"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Just followed link. Bunch of political posts, ice cream stands and star trek jokes. I think I get better content on <a href="/r/cybersecurity">r/cybersecurity</a> and arstechnica</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1foxtg/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1foxtg/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1f0eas" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1foxtg"></div></div><div class="child"><div id="siteTable_t1_k1foxtg" class="sitetable listing"><div class=" thing id-t1_k1fqxd3 noncollapsed   comment " id="thing_t1_k1fqxd3" onclick="click_thing(this)" data-fullname="t1_k1fqxd3" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="moker" data-author-fullname="t2_31fvu" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fqxd3/" ><p class="parent"><a name="k1fqxd3"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/moker" class="author may-blank id-t2_31fvu" >moker</a><span class="userattrs"></span> <span class="score dislikes" title="4">4 points</span><span class="score unvoted" title="5">5 points</span><span class="score likes" title="6">6 points</span> <time title="Wed Sep 20 16:37:04 2023 UTC" datetime="2023-09-20T16:37:04+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(2 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1fqxd39lu"><input type="hidden" name="thing_id" value="t1_k1fqxd3"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>I don't think you were looking at the correct timeline - this is more representative of what we see: <a href="https://infosec.exchange/public/local">https://infosec.exchange/public/local</a></p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fqxd3/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fqxd3/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1foxtg" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1fqxd3"></div></div><div class="child"><div id="siteTable_t1_k1fqxd3" class="sitetable listing"><div class=" thing noncollapsed   deleted controversial comment " onclick="click_thing(this)" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fyl9y/" ><p class="parent"></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><em>[deleted]</em> <time title="Wed Sep 20 17:21:40 2023 UTC" datetime="2023-09-20T17:21:40+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(1 child)</a></p><div class="usertext grayed"><input type="hidden" name="thing_id" value=""/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>[deleted]</p>
+</div>
+</div></div><ul class="flat-list buttons"></ul><div class="reportform report-t1_k1fyl9y"></div></div><div class="child"><div id="siteTable_deleted" class="sitetable listing"><div class=" thing id-t1_k1fzrwf noncollapsed   comment " id="thing_t1_k1fzrwf" onclick="click_thing(this)" data-fullname="t1_k1fzrwf" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="moker" data-author-fullname="t2_31fvu" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fzrwf/" ><p class="parent"><a name="k1fzrwf"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/moker" class="author may-blank id-t2_31fvu" >moker</a><span class="userattrs"></span> <span class="score dislikes" title="7">7 points</span><span class="score unvoted" title="8">8 points</span><span class="score likes" title="9">9 points</span> <time title="Wed Sep 20 17:28:28 2023 UTC" datetime="2023-09-20T17:28:28+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1fzrwf2yq"><input type="hidden" name="thing_id" value="t1_k1fzrwf"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Thanks for giving it a look.</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fzrwf/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fzrwf/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1fzrwf"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1fmsen noncollapsed   comment " id="thing_t1_k1fmsen" onclick="click_thing(this)" data-fullname="t1_k1fmsen" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="Individual-Ad-9902" data-author-fullname="t2_75h3ss5c" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fmsen/" ><p class="parent"><a name="k1fmsen"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/Individual-Ad-9902" class="author may-blank id-t2_75h3ss5c" >Individual-Ad-9902</a><span class="userattrs"></span> <span class="score dislikes" title="0">0 points</span><span class="score unvoted" title="1">1 point</span><span class="score likes" title="2">2 points</span> <time title="Wed Sep 20 16:12:49 2023 UTC" datetime="2023-09-20T16:12:49+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1fmsen3eo"><input type="hidden" name="thing_id" value="t1_k1fmsen"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>SECOND!</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fmsen/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fmsen/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1f0eas" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1fmsen"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1ee5j5 noncollapsed   comment " id="thing_t1_k1ee5j5" onclick="click_thing(this)" data-fullname="t1_k1ee5j5" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="TradeApe" data-author-fullname="t2_51lfc8p8" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1ee5j5/" ><p class="parent"><a name="k1ee5j5"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/TradeApe" class="author may-blank id-t2_51lfc8p8" >TradeApe</a><span class="userattrs"></span> <span class="score dislikes" title="37">37 points</span><span class="score unvoted" title="38">38 points</span><span class="score likes" title="39">39 points</span> <time title="Wed Sep 20 10:58:41 2023 UTC" datetime="2023-09-20T10:58:41+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1ee5j50pl"><input type="hidden" name="thing_id" value="t1_k1ee5j5"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Infosec.exchange mastodon server</p>
+
+<p>X feels too much like Rumble or an Alex Jones fan club with the content that gets pushed. Definitely not paying for that.</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1ee5j5/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1ee5j5/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1ee5j5"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1ek11e noncollapsed   comment " id="thing_t1_k1ek11e" onclick="click_thing(this)" data-fullname="t1_k1ek11e" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="Doc_Hobb" data-author-fullname="t2_5f8eu" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1ek11e/" ><p class="parent"><a name="k1ek11e"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/Doc_Hobb" class="author may-blank id-t2_5f8eu" >Doc_Hobb</a><span class="flairrichtext flaircolorlight flair " title="Vulnerability Researcher" style="background-color: #373c3f; border-color: #373c3f;"><span>Vulnerability Researcher</span></span><span class="userattrs"></span> <span class="score dislikes" title="17">17 points</span><span class="score unvoted" title="18">18 points</span><span class="score likes" title="19">19 points</span> <time title="Wed Sep 20 11:53:33 2023 UTC" datetime="2023-09-20T11:53:33+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1ek11e6rl"><input type="hidden" name="thing_id" value="t1_k1ek11e"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>I like to use <a href="https://allinfosecnews.com">https://allinfosecnews.com</a> it’s a great collection of feeds</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1ek11e/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1ek11e/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1ek11e"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1he78h noncollapsed   comment " id="thing_t1_k1he78h" onclick="click_thing(this)" data-fullname="t1_k1he78h" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="Rebootkid" data-author-fullname="t2_4fhi9" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1he78h/" ><p class="parent"><a name="k1he78h"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/Rebootkid" class="author may-blank id-t2_4fhi9" >Rebootkid</a><span class="userattrs"></span> <span class="score dislikes" title="4">4 points</span><span class="score unvoted" title="5">5 points</span><span class="score likes" title="6">6 points</span> <time title="Wed Sep 20 22:19:37 2023 UTC" datetime="2023-09-20T22:19:37+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(1 child)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1he78hf0b"><input type="hidden" name="thing_id" value="t1_k1he78h"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>I'm over on infosec.exchange. Found it very useful.</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1he78h/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1he78h/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1he78h"></div></div><div class="child"><div id="siteTable_t1_k1he78h" class="sitetable listing"><div class=" thing id-t1_k1i0icy noncollapsed   comment " id="thing_t1_k1i0icy" onclick="click_thing(this)" data-fullname="t1_k1i0icy" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="hudsoncress" data-author-fullname="t2_18vpapcs" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1i0icy/" ><p class="parent"><a name="k1i0icy"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/hudsoncress" class="author may-blank id-t2_18vpapcs" >hudsoncress</a><span class="userattrs"></span> <span class="score dislikes" title="0">0 points</span><span class="score unvoted" title="1">1 point</span><span class="score likes" title="2">2 points</span> <time title="Thu Sep 21 00:52:19 2023 UTC" datetime="2023-09-21T00:52:19+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1i0icygih"><input type="hidden" name="thing_id" value="t1_k1i0icy"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>This is the way</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1i0icy/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1i0icy/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1he78h" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1i0icy"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1erfy3 noncollapsed   comment " id="thing_t1_k1erfy3" onclick="click_thing(this)" data-fullname="t1_k1erfy3" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1erfy3/" ><p class="parent"><a name="k1erfy3"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><span>[deleted]</span> <span class="score dislikes" title="10">10 points</span><span class="score unvoted" title="11">11 points</span><span class="score likes" title="12">12 points</span> <time title="Wed Sep 20 12:52:21 2023 UTC" datetime="2023-09-20T12:52:21+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(3 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1erfy37b8"><input type="hidden" name="thing_id" value="t1_k1erfy3"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>What I find interesting is after I closed my account, over time I realized I was gradually being linked to Twitter less and less by other external websites/ news sites. Now weeks can go by without it happening. </p>
+
+<p>So if his goal is to make twitter no longer relevant, he's doing a banger job.</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1erfy3/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1erfy3/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1erfy3"></div></div><div class="child"><div id="siteTable_t1_k1erfy3" class="sitetable listing"><div class=" thing id-t1_k1f72xa noncollapsed   comment " id="thing_t1_k1f72xa" onclick="click_thing(this)" data-fullname="t1_k1f72xa" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="itwasaraccoon" data-author-fullname="t2_tyqigoko" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f72xa/" ><p class="parent"><a name="k1f72xa"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/itwasaraccoon" class="author may-blank id-t2_tyqigoko" >itwasaraccoon</a><span class="userattrs"></span> <span class="score dislikes" title="2">2 points</span><span class="score unvoted" title="3">3 points</span><span class="score likes" title="4">4 points</span> <time title="Wed Sep 20 14:38:09 2023 UTC" datetime="2023-09-20T14:38:09+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(2 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1f72xa4h4"><input type="hidden" name="thing_id" value="t1_k1f72xa"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Same with me. But I have to admit that the huge security community and information exchange on Twitter used to be super helpful to stay up to date. Its going to take a long time to replicate that somewhere else.</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f72xa/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f72xa/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1erfy3" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1f72xa"></div></div><div class="child"><div id="siteTable_t1_k1f72xa" class="sitetable listing"><div class=" thing id-t1_k1fz12d noncollapsed   comment " id="thing_t1_k1fz12d" onclick="click_thing(this)" data-fullname="t1_k1fz12d" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="650REDHAIR" data-author-fullname="t2_18ip5p" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fz12d/" ><p class="parent"><a name="k1fz12d"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/650REDHAIR" class="author may-blank id-t2_18ip5p" >650REDHAIR</a><span class="userattrs"></span> <span class="score dislikes" title="0">0 points</span><span class="score unvoted" title="1">1 point</span><span class="score likes" title="2">2 points</span> <time title="Wed Sep 20 17:24:11 2023 UTC" datetime="2023-09-20T17:24:11+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(1 child)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1fz12dbgj"><input type="hidden" name="thing_id" value="t1_k1fz12d"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>My tinfoil hat might be on too tight, but sometimes I wonder if that is by design.</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fz12d/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fz12d/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1f72xa" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1fz12d"></div></div><div class="child"><div id="siteTable_t1_k1fz12d" class="sitetable listing"><div class=" thing id-t1_k1g1jnw noncollapsed   comment " id="thing_t1_k1g1jnw" onclick="click_thing(this)" data-fullname="t1_k1g1jnw" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="itwasaraccoon" data-author-fullname="t2_tyqigoko" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1g1jnw/" ><p class="parent"><a name="k1g1jnw"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/itwasaraccoon" class="author may-blank id-t2_tyqigoko" >itwasaraccoon</a><span class="userattrs"></span> <span class="score dislikes" title="0">0 points</span><span class="score unvoted" title="1">1 point</span><span class="score likes" title="2">2 points</span> <time title="Wed Sep 20 17:38:42 2023 UTC" datetime="2023-09-20T17:38:42+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1g1jnwdk1"><input type="hidden" name="thing_id" value="t1_k1g1jnw"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Have you tried the new Titanium hat instead? People seem to love the color at least.</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1g1jnw/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1g1jnw/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1fz12d" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1g1jnw"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1dztic noncollapsed   comment " id="thing_t1_k1dztic" onclick="click_thing(this)" data-fullname="t1_k1dztic" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="AyeSocketFucker" data-author-fullname="t2_9dx76" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1dztic/" ><p class="parent"><a name="k1dztic"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/AyeSocketFucker" class="author may-blank id-t2_9dx76" >AyeSocketFucker</a><span class="userattrs"></span> <span class="score dislikes" title="8">8 points</span><span class="score unvoted" title="9">9 points</span><span class="score likes" title="10">10 points</span> <time title="Wed Sep 20 07:58:53 2023 UTC" datetime="2023-09-20T07:58:53+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(4 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1dzticb63"><input type="hidden" name="thing_id" value="t1_k1dztic"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>It was mastodon, not sure anymore, haven’t used it in awhile</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1dztic/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1dztic/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1dztic"></div></div><div class="child"><div id="siteTable_t1_k1dztic" class="sitetable listing"><div class=" thing id-t1_k1fwapb noncollapsed   comment " id="thing_t1_k1fwapb" onclick="click_thing(this)" data-fullname="t1_k1fwapb" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="vitriolix" data-author-fullname="t2_38fl0" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fwapb/" ><p class="parent"><a name="k1fwapb"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/vitriolix" class="author may-blank id-t2_38fl0" >vitriolix</a><span class="userattrs"></span> <span class="score dislikes" title="5">5 points</span><span class="score unvoted" title="6">6 points</span><span class="score likes" title="7">7 points</span> <time title="Wed Sep 20 17:08:24 2023 UTC" datetime="2023-09-20T17:08:24+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(3 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1fwapbn9u"><input type="hidden" name="thing_id" value="t1_k1fwapb"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Masto is thriving and growing rapidly, up to 14mil created accounts now (and of course lower monthly actives, but still very active). Every time there is news of more twitter stupidity there is a new spike of signups</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fwapb/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fwapb/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1dztic" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1fwapb"></div></div><div class="child"><div id="siteTable_t1_k1fwapb" class="sitetable listing"><div class=" thing id-t1_k1h0z54 noncollapsed   comment " id="thing_t1_k1h0z54" onclick="click_thing(this)" data-fullname="t1_k1h0z54" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1h0z54/" ><p class="parent"><a name="k1h0z54"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><span>[deleted]</span> <span class="score dislikes" title="0">0 points</span><span class="score unvoted" title="1">1 point</span><span class="score likes" title="2">2 points</span> <time title="Wed Sep 20 20:59:56 2023 UTC" datetime="2023-09-20T20:59:56+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(2 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1h0z54yqx"><input type="hidden" name="thing_id" value="t1_k1h0z54"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>I tried Mastadon but I found it very difficult to actually see any posts that were actually worthwhile or interesting. It seems their algorithms for content recommendation need a lot of work or don't exist.</p>
+
+<p>I don't care about who posted most recently, I want to know what's actually worth reading that day.</p>
+
+<p>Also tried Threads but found it difficult to even find the content I wanted to see.</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1h0z54/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1h0z54/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1fwapb" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1h0z54"></div></div><div class="child"><div id="siteTable_t1_k1h0z54" class="sitetable listing"><div class=" thing id-t1_k1hfmcg noncollapsed   comment " id="thing_t1_k1hfmcg" onclick="click_thing(this)" data-fullname="t1_k1hfmcg" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="coloRD" data-author-fullname="t2_8d2bp" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1hfmcg/" ><p class="parent"><a name="k1hfmcg"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/coloRD" class="author may-blank id-t2_8d2bp" >coloRD</a><span class="userattrs"></span> <span class="score dislikes" title="0">0 points</span><span class="score unvoted" title="1">1 point</span><span class="score likes" title="2">2 points</span> <time title="Wed Sep 20 22:28:54 2023 UTC" datetime="2023-09-20T22:28:54+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(1 child)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1hfmcg501"><input type="hidden" name="thing_id" value="t1_k1hfmcg"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>it is based more on hashtags and you choosing who to follow than recommender algorithms. In fact many mastodon users often proudly proclaim they do not want to live in an algorithmically generated bubble being fed content.</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1hfmcg/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1hfmcg/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1h0z54" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1hfmcg"></div></div><div class="child"><div id="siteTable_t1_k1hfmcg" class="sitetable listing"><div class=" thing id-t1_k1jhi39 noncollapsed   comment " id="thing_t1_k1jhi39" onclick="click_thing(this)" data-fullname="t1_k1jhi39" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1jhi39/" ><p class="parent"><a name="k1jhi39"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><span>[deleted]</span> <span class="score dislikes" title="0">0 points</span><span class="score unvoted" title="1">1 point</span><span class="score likes" title="2">2 points</span> <time title="Thu Sep 21 09:27:22 2023 UTC" datetime="2023-09-21T09:27:22+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1jhi39r43"><input type="hidden" name="thing_id" value="t1_k1jhi39"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>It is a double edged sword though as it makes it more difficult to find that content you want to follow - I don't think algorithms are inherently bad as long as they can't be manipulated.</p>
+
+<p>That being said, Reddit mostly managed without algorithms thanks to voting & community driven recommendations, but Mastadon doesn't have that.</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1jhi39/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1jhi39/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1hfmcg" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1jhi39"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1h5u7u noncollapsed   comment " id="thing_t1_k1h5u7u" onclick="click_thing(this)" data-fullname="t1_k1h5u7u" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="AnIrregularRegular" data-author-fullname="t2_164qs4" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1h5u7u/" ><p class="parent"><a name="k1h5u7u"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/AnIrregularRegular" class="author may-blank id-t2_164qs4" >AnIrregularRegular</a><span class="flairrichtext flaircolorlight flair " title="Blue Team" style="background-color: #373c3f; border-color: #373c3f;"><span>Blue Team</span></span><span class="userattrs"></span> <span class="score dislikes" title="2">2 points</span><span class="score unvoted" title="3">3 points</span><span class="score likes" title="4">4 points</span> <time title="Wed Sep 20 21:28:06 2023 UTC" datetime="2023-09-20T21:28:06+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1h5u7ub0u"><input type="hidden" name="thing_id" value="t1_k1h5u7u"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Honestly I’ve yet to run into a great Twitter replacement. Honestly Reddit is maybe the best for stuff before it hits blogs/news which is why I loved Twitter. Go follow sysadmin and MSP and they often see stuff before security people do.</p>
+
+<p>Mastodon is alright but just didn’t scratch the itch the same(also like to follow a lot of foreign policy/natsec peeps who won’t do Mastodon).</p>
+
+<p>Recently got into Bluesky and it’s okay. Not Twitter but also I think has a lot of potential.</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1h5u7u/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1h5u7u/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1h5u7u"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1hk3lb noncollapsed   comment " id="thing_t1_k1hk3lb" onclick="click_thing(this)" data-fullname="t1_k1hk3lb" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="LordCommanderTaurusG" data-author-fullname="t2_v693a42" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1hk3lb/" ><p class="parent"><a name="k1hk3lb"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/LordCommanderTaurusG" class="author may-blank id-t2_v693a42" >LordCommanderTaurusG</a><span class="flairrichtext flaircolorlight flair " title="Blue Team" style="background-color: #373c3f; border-color: #373c3f;"><span>Blue Team</span></span><span class="userattrs"></span> <span class="score dislikes" title="2">2 points</span><span class="score unvoted" title="3">3 points</span><span class="score likes" title="4">4 points</span> <time title="Wed Sep 20 22:59:01 2023 UTC" datetime="2023-09-20T22:59:01+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1hk3lbwhi"><input type="hidden" name="thing_id" value="t1_k1hk3lb"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Have you tried Threads?</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1hk3lb/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1hk3lb/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1hk3lb"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1hvox4 noncollapsed   comment " id="thing_t1_k1hvox4" onclick="click_thing(this)" data-fullname="t1_k1hvox4" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="Tetmohawk" data-author-fullname="t2_2x2zoj2" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1hvox4/" ><p class="parent"><a name="k1hvox4"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/Tetmohawk" class="author may-blank id-t2_2x2zoj2" >Tetmohawk</a><span class="userattrs"></span> <span class="score dislikes" title="2">2 points</span><span class="score unvoted" title="3">3 points</span><span class="score likes" title="4">4 points</span> <time title="Thu Sep 21 00:19:26 2023 UTC" datetime="2023-09-21T00:19:26+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1hvox4c5j"><input type="hidden" name="thing_id" value="t1_k1hvox4"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>I see no ads. Pretty close to never. Here's how:</p>
+
+<p>(1) Use a DNS filter like CleanBrowsing.com and set it to filter ads and tracking.</p>
+
+<p>(2) Put Twitter in its own container. You can do this easily in Firefox. That way cookies and other stuff related to ads is isolated from every other website. </p>
+
+<p>(3) Use a Firefox add-on. I use both Privacy badger and uBlock origin.</p>
+
+<p>I don't see ads on almost any site with this method. At work I get ads all the time and it's annoying. Not sure how y'all lived like this for so long. Ads haven't been a part of my life for years. Now you know why.</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1hvox4/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1hvox4/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1hvox4"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1eii4j noncollapsed   comment " id="thing_t1_k1eii4j" onclick="click_thing(this)" data-fullname="t1_k1eii4j" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="mobo_dojo" data-author-fullname="t2_70dgodgm" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1eii4j/" ><p class="parent"><a name="k1eii4j"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/mobo_dojo" class="author may-blank id-t2_70dgodgm" >mobo_dojo</a><span class="userattrs"></span> <span class="score dislikes" title="4">4 points</span><span class="score unvoted" title="5">5 points</span><span class="score likes" title="6">6 points</span> <time title="Wed Sep 20 11:40:21 2023 UTC" datetime="2023-09-20T11:40:21+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(1 child)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1eii4j0n0"><input type="hidden" name="thing_id" value="t1_k1eii4j"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Newsboat</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1eii4j/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1eii4j/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1eii4j"></div></div><div class="child"><div id="siteTable_t1_k1eii4j" class="sitetable listing"><div class=" thing id-t1_k1ftwx9 noncollapsed   comment " id="thing_t1_k1ftwx9" onclick="click_thing(this)" data-fullname="t1_k1ftwx9" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="irkine" data-author-fullname="t2_katin" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1ftwx9/" ><p class="parent"><a name="k1ftwx9"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/irkine" class="author may-blank id-t2_katin" >irkine</a><span class="userattrs"></span> <span class="score dislikes" title="0">0 points</span><span class="score unvoted" title="1">1 point</span><span class="score likes" title="2">2 points</span> <time title="Wed Sep 20 16:54:33 2023 UTC" datetime="2023-09-20T16:54:33+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1ftwx9dzw"><input type="hidden" name="thing_id" value="t1_k1ftwx9"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>What does your feed list look like for security? :)</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1ftwx9/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1ftwx9/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1eii4j" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1ftwx9"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1fdb6p noncollapsed   comment " id="thing_t1_k1fdb6p" onclick="click_thing(this)" data-fullname="t1_k1fdb6p" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="Versed_Percepton" data-author-fullname="t2_94cm2jqqy" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fdb6p/" ><p class="parent"><a name="k1fdb6p"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/Versed_Percepton" class="author may-blank id-t2_94cm2jqqy" >Versed_Percepton</a><span class="userattrs"></span> <span class="score dislikes" title="3">3 points</span><span class="score unvoted" title="4">4 points</span><span class="score likes" title="5">5 points</span> <time title="Wed Sep 20 15:16:29 2023 UTC" datetime="2023-09-20T15:16:29+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1fdb6pxm7"><input type="hidden" name="thing_id" value="t1_k1fdb6p"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>The closest thing to Twitter would be mastodon right now. You just need to decide on your home instance, build your profile like you would anywhere else, and start finding topics, hashtags, and people/groups to follow. Then filter out the junk(you can black list on keywords) so you can rebuild your RSS like you have it setup on Twitter. </p>
+
+<p>There are a dozen or so Infosec instances to choose from, I like <a href="https://Infosec.Exchange" rel="nofollow">Infosec.Exchange</a> as its stable and a smooth experience. It has a solid Admin team and has no issues talking to the federation. The membership on the instance is pretty open and welcoming.</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fdb6p/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fdb6p/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1fdb6p"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1fn2i6 noncollapsed   comment " id="thing_t1_k1fn2i6" onclick="click_thing(this)" data-fullname="t1_k1fn2i6" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="Individual-Ad-9902" data-author-fullname="t2_75h3ss5c" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fn2i6/" ><p class="parent"><a name="k1fn2i6"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/Individual-Ad-9902" class="author may-blank id-t2_75h3ss5c" >Individual-Ad-9902</a><span class="userattrs"></span> <span class="score dislikes" title="1">1 point</span><span class="score unvoted" title="2">2 points</span><span class="score likes" title="3">3 points</span> <time title="Wed Sep 20 16:14:27 2023 UTC" datetime="2023-09-20T16:14:27+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1fn2i6urt"><input type="hidden" name="thing_id" value="t1_k1fn2i6"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Infosec.exchange on Mastodon is a very good place, and I get a lot of good information from my curated group on Linkedin. Dr. Chase Cunningham has a good weekly wrap up. And then there is always Cyber Protection Magazine.</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fn2i6/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fn2i6/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1fn2i6"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1encn2 noncollapsed   comment " id="thing_t1_k1encn2" onclick="click_thing(this)" data-fullname="t1_k1encn2" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="eat_the_pennies" data-author-fullname="t2_vrscons4" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1encn2/" ><p class="parent"><a name="k1encn2"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/eat_the_pennies" class="author may-blank id-t2_vrscons4" >eat_the_pennies</a><span class="flairrichtext flaircolorlight flair " title="System Administrator" style="background-color: #373c3f; border-color: #373c3f;"><span>System Administrator</span></span><span class="userattrs"></span> <span class="score dislikes" title="2">2 points</span><span class="score unvoted" title="3">3 points</span><span class="score likes" title="4">4 points</span> <time title="Wed Sep 20 12:20:49 2023 UTC" datetime="2023-09-20T12:20:49+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(7 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1encn2edi"><input type="hidden" name="thing_id" value="t1_k1encn2"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>I'm hoping Bluesky gets more popular once it actually opens. I was able to join yesterday and there's a small community of infosec people who share news there.</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1encn2/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1encn2/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1encn2"></div></div><div class="child"><div id="siteTable_t1_k1encn2" class="sitetable listing"><div class=" thing id-t1_k1f55vs noncollapsed   comment " id="thing_t1_k1f55vs" onclick="click_thing(this)" data-fullname="t1_k1f55vs" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="SpaceMaxil" data-author-fullname="t2_iw8rdehp" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f55vs/" ><p class="parent"><a name="k1f55vs"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/SpaceMaxil" class="author may-blank id-t2_iw8rdehp" >SpaceMaxil</a><span class="userattrs"></span> <span class="score dislikes" title="1">1 point</span><span class="score unvoted" title="2">2 points</span><span class="score likes" title="3">3 points</span> <time title="Wed Sep 20 14:26:15 2023 UTC" datetime="2023-09-20T14:26:15+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(4 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1f55vsbw3"><input type="hidden" name="thing_id" value="t1_k1f55vs"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Seems the chatty security folks are pretty split on Mastadon vs BlueSky. But most of the good leaks still end up on Twitter first.</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f55vs/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f55vs/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1encn2" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1f55vs"></div></div><div class="child"><div id="siteTable_t1_k1f55vs" class="sitetable listing"><div class=" thing id-t1_k1f6e6p noncollapsed   comment " id="thing_t1_k1f6e6p" onclick="click_thing(this)" data-fullname="t1_k1f6e6p" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="eat_the_pennies" data-author-fullname="t2_vrscons4" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f6e6p/" ><p class="parent"><a name="k1f6e6p"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/eat_the_pennies" class="author may-blank id-t2_vrscons4" >eat_the_pennies</a><span class="flairrichtext flaircolorlight flair " title="System Administrator" style="background-color: #373c3f; border-color: #373c3f;"><span>System Administrator</span></span><span class="userattrs"></span> <span class="score dislikes" title="1">1 point</span><span class="score unvoted" title="2">2 points</span><span class="score likes" title="3">3 points</span> <time title="Wed Sep 20 14:33:58 2023 UTC" datetime="2023-09-20T14:33:58+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(3 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1f6e6phz7"><input type="hidden" name="thing_id" value="t1_k1f6e6p"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Mastodon would've taken off by now if it really was ideal imo. The hesitancy leads me to believe people are really holding out for Bluesky to be Twitter 2.0</p>
+
+<p>Who knows if we'll ever get to that point though</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f6e6p/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f6e6p/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1f55vs" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1f6e6p"></div></div><div class="child"><div id="siteTable_t1_k1f6e6p" class="sitetable listing"><div class=" thing id-t1_k1f743s noncollapsed   comment " id="thing_t1_k1f743s" onclick="click_thing(this)" data-fullname="t1_k1f743s" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="SpaceMaxil" data-author-fullname="t2_iw8rdehp" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f743s/" ><p class="parent"><a name="k1f743s"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/SpaceMaxil" class="author may-blank id-t2_iw8rdehp" >SpaceMaxil</a><span class="userattrs"></span> <span class="score dislikes" title="1">1 point</span><span class="score unvoted" title="2">2 points</span><span class="score likes" title="3">3 points</span> <time title="Wed Sep 20 14:38:21 2023 UTC" datetime="2023-09-20T14:38:21+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1f743sn6p"><input type="hidden" name="thing_id" value="t1_k1f743s"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Folks are also looking for apps that work across fediverses. Seems to have potential.</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f743s/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f743s/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1f6e6p" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1f743s"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1flu07 noncollapsed   comment " id="thing_t1_k1flu07" onclick="click_thing(this)" data-fullname="t1_k1flu07" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="look_ima_frog" data-author-fullname="t2_hbyptyxz" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1flu07/" ><p class="parent"><a name="k1flu07"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/look_ima_frog" class="author may-blank id-t2_hbyptyxz" >look_ima_frog</a><span class="userattrs"></span> <span class="score dislikes" title="1">1 point</span><span class="score unvoted" title="2">2 points</span><span class="score likes" title="3">3 points</span> <time title="Wed Sep 20 16:07:07 2023 UTC" datetime="2023-09-20T16:07:07+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(1 child)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1flu07ulq"><input type="hidden" name="thing_id" value="t1_k1flu07"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>I'm like a lot of people who have tried mastodon and nope out. </p>
+
+<p>I get the idea, but I still don't care to use it. User experience is not good. I have enough to learn and fix for my work, dealing with an overwrought platform isn't on the list right now.</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1flu07/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1flu07/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1f6e6p" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1flu07"></div></div><div class="child"><div id="siteTable_t1_k1flu07" class="sitetable listing"><div class=" thing id-t1_k1gstv3 noncollapsed   comment " id="thing_t1_k1gstv3" onclick="click_thing(this)" data-fullname="t1_k1gstv3" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="Slythela" data-author-fullname="t2_ixk2h" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1gstv3/" ><p class="parent"><a name="k1gstv3"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/Slythela" class="author may-blank id-t2_ixk2h" >Slythela</a><span class="userattrs"></span> <span class="score dislikes" title="1">1 point</span><span class="score unvoted" title="2">2 points</span><span class="score likes" title="3">3 points</span> <time title="Wed Sep 20 20:12:56 2023 UTC" datetime="2023-09-20T20:12:56+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1gstv3860"><input type="hidden" name="thing_id" value="t1_k1gstv3"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>I'm the same way. I was pretty pumped to have another platform, I'm pretty over this website and I've never been into twitter. It's just not really there yet though, and it feels more like a facebook feed than anything else, even on the infosec ones.</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1gstv3/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1gstv3/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1flu07" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1gstv3"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1ewj8c noncollapsed   comment " id="thing_t1_k1ewj8c" onclick="click_thing(this)" data-fullname="t1_k1ewj8c" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="flylikegaruda" data-author-fullname="t2_1189kv" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1ewj8c/" ><p class="parent"><a name="k1ewj8c"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/flylikegaruda" class="author may-blank id-t2_1189kv" >flylikegaruda</a><span class="flairrichtext flaircolorlight flair " title="Red Team" style="background-color: #373c3f; border-color: #373c3f;"><span>Red Team</span></span><span class="userattrs"></span> <span class="score dislikes" title="0">0 points</span><span class="score unvoted" title="1">1 point</span><span class="score likes" title="2">2 points</span> <time title="Wed Sep 20 13:29:00 2023 UTC" datetime="2023-09-20T13:29:00+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(1 child)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1ewj8c5y7"><input type="hidden" name="thing_id" value="t1_k1ewj8c"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Yes, but I get more cat pics than security. I am no fan of cats!</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1ewj8c/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1ewj8c/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1encn2" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1ewj8c"></div></div><div class="child"><div id="siteTable_t1_k1ewj8c" class="sitetable listing"><div class=" thing id-t1_k1f3pc2 noncollapsed   comment " id="thing_t1_k1f3pc2" onclick="click_thing(this)" data-fullname="t1_k1f3pc2" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="eat_the_pennies" data-author-fullname="t2_vrscons4" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f3pc2/" ><p class="parent"><a name="k1f3pc2"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/eat_the_pennies" class="author may-blank id-t2_vrscons4" >eat_the_pennies</a><span class="flairrichtext flaircolorlight flair " title="System Administrator" style="background-color: #373c3f; border-color: #373c3f;"><span>System Administrator</span></span><span class="userattrs"></span> <span class="score dislikes" title="2">2 points</span><span class="score unvoted" title="3">3 points</span><span class="score likes" title="4">4 points</span> <time title="Wed Sep 20 14:17:05 2023 UTC" datetime="2023-09-20T14:17:05+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1f3pc255m"><input type="hidden" name="thing_id" value="t1_k1f3pc2"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Unfortunate, cats are a huge part of my life :)</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f3pc2/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f3pc2/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1ewj8c" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1f3pc2"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1e9gx9 noncollapsed   controversial comment " id="thing_t1_k1e9gx9" onclick="click_thing(this)" data-fullname="t1_k1e9gx9" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="asecuredlife" data-author-fullname="t2_57r4boy9" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1e9gx9/" ><p class="parent"><a name="k1e9gx9"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/asecuredlife" class="author may-blank id-t2_57r4boy9" >asecuredlife</a><span class="userattrs"></span> <span class="score dislikes" title="1">1 point</span><span class="score unvoted" title="2">2 points</span><span class="score likes" title="3">3 points</span> <time title="Wed Sep 20 10:05:13 2023 UTC" datetime="2023-09-20T10:05:13+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(4 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1e9gx9755"><input type="hidden" name="thing_id" value="t1_k1e9gx9"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Weirder? Twitter has always been a weird place.</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1e9gx9/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1e9gx9/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1e9gx9"></div></div><div class="child"><div id="siteTable_t1_k1e9gx9" class="sitetable listing"><div class=" thing id-t1_k1eb9de noncollapsed   comment " id="thing_t1_k1eb9de" onclick="click_thing(this)" data-fullname="t1_k1eb9de" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="skeedooshski" data-author-fullname="t2_2w3lp8yl" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1eb9de/" ><p class="parent"><a name="k1eb9de"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/skeedooshski" class="author submitter may-blank id-t2_2w3lp8yl" >skeedooshski</a><span class="userattrs">[<a class="submitter" title="submitter" href="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/">S</a>]</span> <span class="score dislikes" title="20">20 points</span><span class="score unvoted" title="21">21 points</span><span class="score likes" title="22">22 points</span> <time title="Wed Sep 20 10:26:28 2023 UTC" datetime="2023-09-20T10:26:28+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(1 child)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1eb9dedum"><input type="hidden" name="thing_id" value="t1_k1eb9de"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Hence the weirdER :). Initially a part of its appeal, but increasingly not the case as of lately.</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1eb9de/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1eb9de/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1e9gx9" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1eb9de"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1eszi4 noncollapsed   comment " id="thing_t1_k1eszi4" onclick="click_thing(this)" data-fullname="t1_k1eszi4" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="ComfortableProperty9" data-author-fullname="t2_3s11dgpn" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1eszi4/" ><p class="parent"><a name="k1eszi4"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/ComfortableProperty9" class="author may-blank id-t2_3s11dgpn" >ComfortableProperty9</a><span class="userattrs"></span> <span class="score dislikes" title="5">5 points</span><span class="score unvoted" title="6">6 points</span><span class="score likes" title="7">7 points</span> <time title="Wed Sep 20 13:03:51 2023 UTC" datetime="2023-09-20T13:03:51+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(1 child)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1eszi4s4l"><input type="hidden" name="thing_id" value="t1_k1eszi4"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>It went to shit right as I got my feed cultivated exactly like I wanted it.</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1eszi4/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1eszi4/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1e9gx9" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1eszi4"></div></div><div class="child"><div id="siteTable_t1_k1eszi4" class="sitetable listing"><div class=" thing id-t1_k1eubmi noncollapsed   comment " id="thing_t1_k1eubmi" onclick="click_thing(this)" data-fullname="t1_k1eubmi" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="missed_sla" data-author-fullname="t2_1pw5guwr" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1eubmi/" ><p class="parent"><a name="k1eubmi"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/missed_sla" class="author may-blank id-t2_1pw5guwr" >missed_sla</a><span class="userattrs"></span> <span class="score dislikes" title="7">7 points</span><span class="score unvoted" title="8">8 points</span><span class="score likes" title="9">9 points</span> <time title="Wed Sep 20 13:13:34 2023 UTC" datetime="2023-09-20T13:13:34+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1eubmisqg"><input type="hidden" name="thing_id" value="t1_k1eubmi"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Twitter is a hydra of insane conservatives now. Block one and 5 more are shoved into your face. My block list hundreds long and growing every time I load up that goddamn website. Honestly don't know why I do anymore, it's usually about 30 seconds before I close it again.</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1eubmi/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1eubmi/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1eszi4" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1eubmi"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1ewh6y noncollapsed   comment " id="thing_t1_k1ewh6y" onclick="click_thing(this)" data-fullname="t1_k1ewh6y" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="WummageSail" data-author-fullname="t2_14ajti" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1ewh6y/" ><p class="parent"><a name="k1ewh6y"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/WummageSail" class="author may-blank id-t2_14ajti" >WummageSail</a><span class="userattrs"></span> <span class="score dislikes" title="1">1 point</span><span class="score unvoted" title="2">2 points</span><span class="score likes" title="3">3 points</span> <time title="Wed Sep 20 13:28:36 2023 UTC" datetime="2023-09-20T13:28:36+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(1 child)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1ewh6yuvf"><input type="hidden" name="thing_id" value="t1_k1ewh6y"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p><a href="https://risky.biz/" rel="nofollow">https://risky.biz/</a> podcast</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1ewh6y/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1ewh6y/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1ewh6y"></div></div><div class="child"><div id="siteTable_t1_k1ewh6y" class="sitetable listing"><div class=" thing id-t1_k1fhicr noncollapsed   comment " id="thing_t1_k1fhicr" onclick="click_thing(this)" data-fullname="t1_k1fhicr" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="skeedooshski" data-author-fullname="t2_2w3lp8yl" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fhicr/" ><p class="parent"><a name="k1fhicr"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/skeedooshski" class="author submitter may-blank id-t2_2w3lp8yl" >skeedooshski</a><span class="userattrs">[<a class="submitter" title="submitter" href="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/">S</a>]</span> <span class="score dislikes" title="0">0 points</span><span class="score unvoted" title="1">1 point</span><span class="score likes" title="2">2 points</span> <time title="Wed Sep 20 15:41:40 2023 UTC" datetime="2023-09-20T15:41:40+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1fhicruwc"><input type="hidden" name="thing_id" value="t1_k1fhicr"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>It is awesome</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fhicr/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fhicr/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1ewh6y" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1fhicr"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1firy2 noncollapsed   comment " id="thing_t1_k1firy2" onclick="click_thing(this)" data-fullname="t1_k1firy2" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="Maidentyone" data-author-fullname="t2_ampb5p3" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1firy2/" ><p class="parent"><a name="k1firy2"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/Maidentyone" class="author may-blank id-t2_ampb5p3" >Maidentyone</a><span class="userattrs"></span> <span class="score dislikes" title="1">1 point</span><span class="score unvoted" title="2">2 points</span><span class="score likes" title="3">3 points</span> <time title="Wed Sep 20 15:49:07 2023 UTC" datetime="2023-09-20T15:49:07+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(2 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1firy2u0d"><input type="hidden" name="thing_id" value="t1_k1firy2"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>I use Feedly it has excellent security feed, plus you can add your own (rss) feeds</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1firy2/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1firy2/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1firy2"></div></div><div class="child"><div id="siteTable_t1_k1firy2" class="sitetable listing"><div class=" thing id-t1_k1fopgv noncollapsed   comment " id="thing_t1_k1fopgv" onclick="click_thing(this)" data-fullname="t1_k1fopgv" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="skeedooshski" data-author-fullname="t2_2w3lp8yl" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fopgv/" ><p class="parent"><a name="k1fopgv"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/skeedooshski" class="author submitter may-blank id-t2_2w3lp8yl" >skeedooshski</a><span class="userattrs">[<a class="submitter" title="submitter" href="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/">S</a>]</span> <span class="score dislikes" title="0">0 points</span><span class="score unvoted" title="1">1 point</span><span class="score likes" title="2">2 points</span> <time title="Wed Sep 20 16:24:03 2023 UTC" datetime="2023-09-20T16:24:03+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1fopgvyrw"><input type="hidden" name="thing_id" value="t1_k1fopgv"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>That's an interesting one. I'll have a look as I'd be keen on adding the risky business RSS feed to something like that.</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fopgv/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fopgv/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1firy2" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1fopgv"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1g2003 noncollapsed   comment " id="thing_t1_k1g2003" onclick="click_thing(this)" data-fullname="t1_k1g2003" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="gamed0g" data-author-fullname="t2_f84b7" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1g2003/" ><p class="parent"><a name="k1g2003"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/gamed0g" class="author may-blank id-t2_f84b7" >gamed0g</a><span class="userattrs"></span> <span class="score dislikes" title="0">0 points</span><span class="score unvoted" title="1">1 point</span><span class="score likes" title="2">2 points</span> <time title="Wed Sep 20 17:41:17 2023 UTC" datetime="2023-09-20T17:41:17+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1g2003bvb"><input type="hidden" name="thing_id" value="t1_k1g2003"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>+1 for Feedly. It has loads of options to configure and refine your feeds</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1g2003/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1g2003/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1firy2" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1g2003"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1fiuv1 noncollapsed   comment " id="thing_t1_k1fiuv1" onclick="click_thing(this)" data-fullname="t1_k1fiuv1" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="HansGuntherboon" data-author-fullname="t2_mx14j" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fiuv1/" ><p class="parent"><a name="k1fiuv1"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/HansGuntherboon" class="author may-blank id-t2_mx14j" >HansGuntherboon</a><span class="userattrs"></span> <span class="score dislikes" title="1">1 point</span><span class="score unvoted" title="2">2 points</span><span class="score likes" title="3">3 points</span> <time title="Wed Sep 20 15:49:36 2023 UTC" datetime="2023-09-20T15:49:36+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1fiuv16ta"><input type="hidden" name="thing_id" value="t1_k1fiuv1"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p><a href="https://infosec.exchange" rel="nofollow">https://infosec.exchange</a></p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fiuv1/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fiuv1/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1fiuv1"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing noncollapsed   deleted controversial comment " onclick="click_thing(this)" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1etkxx/" ><p class="parent"></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><em>[deleted]</em> <time title="Wed Sep 20 13:08:13 2023 UTC" datetime="2023-09-20T13:08:13+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(3 children)</a></p><div class="usertext grayed"><input type="hidden" name="thing_id" value=""/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>[deleted]</p>
+</div>
+</div></div><ul class="flat-list buttons"></ul><div class="reportform report-t1_k1etkxx"></div></div><div class="child"><div id="siteTable_deleted" class="sitetable listing"><div class=" thing id-t1_k1f0w8k noncollapsed   comment " id="thing_t1_k1f0w8k" onclick="click_thing(this)" data-fullname="t1_k1f0w8k" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="moker" data-author-fullname="t2_31fvu" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f0w8k/" ><p class="parent"><a name="k1f0w8k"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/moker" class="author may-blank id-t2_31fvu" >moker</a><span class="userattrs"></span> <span class="score dislikes" title="7">7 points</span><span class="score unvoted" title="8">8 points</span><span class="score likes" title="9">9 points</span> <time title="Wed Sep 20 13:58:43 2023 UTC" datetime="2023-09-20T13:58:43+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1f0w8kcn0"><input type="hidden" name="thing_id" value="t1_k1f0w8k"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Nah, that is not correct. You can join <a href="https://infosec.exchange">infosec.exchange</a> and follow anyone on <a href="https://infosec.exchange">infosec.exchange</a> or any of the other mastodon instances with that one account.</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f0w8k/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f0w8k/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1f0w8k"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1f3wf5 noncollapsed   comment " id="thing_t1_k1f3wf5" onclick="click_thing(this)" data-fullname="t1_k1f3wf5" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="Gangrif" data-author-fullname="t2_13q213" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f3wf5/" ><p class="parent"><a name="k1f3wf5"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/Gangrif" class="author may-blank id-t2_13q213" >Gangrif</a><span class="userattrs"></span> <span class="score dislikes" title="4">4 points</span><span class="score unvoted" title="5">5 points</span><span class="score likes" title="6">6 points</span> <time title="Wed Sep 20 14:18:22 2023 UTC" datetime="2023-09-20T14:18:22+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1f3wf5u1j"><input type="hidden" name="thing_id" value="t1_k1f3wf5"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>no, you’re doing it wrong. you pick a home server based on your preferences. they all federate with eachother. i run my own and the experience has been awesome. you do need to give it time and start following folks to really get involved though.</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f3wf5/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f3wf5/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1f3wf5"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1f1tdt noncollapsed   comment " id="thing_t1_k1f1tdt" onclick="click_thing(this)" data-fullname="t1_k1f1tdt" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="bjh13" data-author-fullname="t2_3s36b" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f1tdt/" ><p class="parent"><a name="k1f1tdt"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/bjh13" class="author may-blank id-t2_3s36b" >bjh13</a><span class="userattrs"></span> <span class="score dislikes" title="9">9 points</span><span class="score unvoted" title="10">10 points</span><span class="score likes" title="11">11 points</span> <time title="Wed Sep 20 14:04:48 2023 UTC" datetime="2023-09-20T14:04:48+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1f1tdtky4"><input type="hidden" name="thing_id" value="t1_k1f1tdt"/><div class="usertext-body may-blank-within md-container " ><div class="md"><blockquote>
+<p>Oh, your people are on 8 different servers so you need 8 different accounts</p>
+</blockquote>
+
+<p>The whole point of being federated is one account allows you to follow people on any of the other servers, so no you don't need 8 different accounts.</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f1tdt/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f1tdt/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1f1tdt"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1eji5w noncollapsed   controversial comment " id="thing_t1_k1eji5w" onclick="click_thing(this)" data-fullname="t1_k1eji5w" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="ThePorko" data-author-fullname="t2_1vnpf8ey" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1eji5w/" ><p class="parent"><a name="k1eji5w"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/ThePorko" class="author may-blank id-t2_1vnpf8ey" >ThePorko</a><span class="userattrs"></span> <span class="score dislikes" title="-2">-2 points</span><span class="score unvoted" title="-1">-1 points</span><span class="score likes" title="0">0 points</span> <time title="Wed Sep 20 11:49:10 2023 UTC" datetime="2023-09-20T11:49:10+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1eji5w59w"><input type="hidden" name="thing_id" value="t1_k1eji5w"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>I have never used twitter for that, too much garbage on there. I tend to do podcasts and youtube media.</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1eji5w/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1eji5w/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1eji5w"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1e0j6k noncollapsed   controversial comment " id="thing_t1_k1e0j6k" onclick="click_thing(this)" data-fullname="t1_k1e0j6k" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1e0j6k/" ><p class="parent"><a name="k1e0j6k"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><span>[deleted]</span> <span class="score dislikes" title="-1">-1 points</span><span class="score unvoted" title="0">0 points</span><span class="score likes" title="1">1 point</span> <time title="Wed Sep 20 08:08:18 2023 UTC" datetime="2023-09-20T08:08:18+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(1 child)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1e0j6khpt"><input type="hidden" name="thing_id" value="t1_k1e0j6k"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>/g/</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1e0j6k/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1e0j6k/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1e0j6k"></div></div><div class="child"><div id="siteTable_t1_k1e0j6k" class="sitetable listing"><div class=" thing id-t1_k1e2rvb noncollapsed   comment " id="thing_t1_k1e2rvb" onclick="click_thing(this)" data-fullname="t1_k1e2rvb" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1e2rvb/" ><p class="parent"><a name="k1e2rvb"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><span>[deleted]</span> <span class="score dislikes" title="6">6 points</span><span class="score unvoted" title="7">7 points</span><span class="score likes" title="8">8 points</span> <time title="Wed Sep 20 08:37:53 2023 UTC" datetime="2023-09-20T08:37:53+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1e2rvbuyx"><input type="hidden" name="thing_id" value="t1_k1e2rvb"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Isn't the /sec/ general completely dead or at the very least only filled with "How to become 1337 haxxor"?</p>
+
+<p>It's been some time since I checked.</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1e2rvb/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1e2rvb/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1e0j6k" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1e2rvb"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1etfwl noncollapsed   comment " id="thing_t1_k1etfwl" onclick="click_thing(this)" data-fullname="t1_k1etfwl" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="chicagoandy" data-author-fullname="t2_92ggj" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1etfwl/" ><p class="parent"><a name="k1etfwl"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/chicagoandy" class="author may-blank id-t2_92ggj" >chicagoandy</a><span class="userattrs"></span> <span class="score dislikes" title="-1">-1 points</span><span class="score unvoted" title="0">0 points</span><span class="score likes" title="1">1 point</span> <time title="Wed Sep 20 13:07:11 2023 UTC" datetime="2023-09-20T13:07:11+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1etfwlrm3"><input type="hidden" name="thing_id" value="t1_k1etfwl"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Um... Reddit.</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1etfwl/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1etfwl/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1etfwl"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1f1b06 noncollapsed   comment " id="thing_t1_k1f1b06" onclick="click_thing(this)" data-fullname="t1_k1f1b06" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="Chrishamilton2007" data-author-fullname="t2_5goj3" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f1b06/" ><p class="parent"><a name="k1f1b06"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/Chrishamilton2007" class="author may-blank id-t2_5goj3" >Chrishamilton2007</a><span class="userattrs"></span> <span class="score dislikes" title="-1">-1 points</span><span class="score unvoted" title="0">0 points</span><span class="score likes" title="1">1 point</span> <time title="Wed Sep 20 14:01:25 2023 UTC" datetime="2023-09-20T14:01:25+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1f1b06o27"><input type="hidden" name="thing_id" value="t1_k1f1b06"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>You can use reddit.</p>
+
+<p><a href="https://www.reddit.com/r/CASB+HackBloc+Malware+REMath+ReverseEngineering+blackhat+blueteamsec+computerforensics+crypto+netsec+netsecstudents+cyber+pwned+rootkit+vrd+xss+InfoSecInsiders/top/?sort=top&t=day" rel="nofollow">https://www.reddit.com/r/CASB+HackBloc+Malware+REMath+ReverseEngineering+blackhat+blueteamsec+computerforensics+crypto+netsec+netsecstudents+cyber+pwned+rootkit+vrd+xss+InfoSecInsiders/top/?sort=top&t=day</a></p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f1b06/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1f1b06/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1f1b06"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1h24vs noncollapsed   comment " id="thing_t1_k1h24vs" onclick="click_thing(this)" data-fullname="t1_k1h24vs" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="Bllago" data-author-fullname="t2_5e3g42ih" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1h24vs/" ><p class="parent"><a name="k1h24vs"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/Bllago" class="author may-blank id-t2_5e3g42ih" >Bllago</a><span class="userattrs"></span> <span class="score dislikes" title="-1">-1 points</span><span class="score unvoted" title="0">0 points</span><span class="score likes" title="1">1 point</span> <time title="Wed Sep 20 21:06:31 2023 UTC" datetime="2023-09-20T21:06:31+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1h24vsydi"><input type="hidden" name="thing_id" value="t1_k1h24vs"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Twitter is full of CSAM. Everyone needs to leave it.</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1h24vs/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1h24vs/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1h24vs"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1eqh9b collapsed collapsed-for-reason   comment " id="thing_t1_k1eqh9b" onclick="click_thing(this)" data-fullname="t1_k1eqh9b" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="KidBeene" data-author-fullname="t2_bfou7" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1eqh9b/" ><p class="parent"><a name="k1eqh9b"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[+]</a><a href="https://www.reddit.com/user/KidBeene" class="author may-blank id-t2_bfou7" >KidBeene</a><span class="userattrs"></span> <span class="collapsed-reason">comment score below threshold</span><span class="score dislikes" title="-11">-11 points</span><span class="score unvoted" title="-10">-10 points</span><span class="score likes" title="-9">-9 points</span> <time title="Wed Sep 20 12:45:06 2023 UTC" datetime="2023-09-20T12:45:06+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(2 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1eqh9blt7"><input type="hidden" name="thing_id" value="t1_k1eqh9b"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>You were using Twitter for industry news? LOL Oh man... how much time do you have in the day?</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1eqh9b/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1eqh9b/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1eqh9b"></div></div><div class="child"><div id="siteTable_t1_k1eqh9b" class="sitetable listing"><div class=" thing id-t1_k1euxho noncollapsed   comment " id="thing_t1_k1euxho" onclick="click_thing(this)" data-fullname="t1_k1euxho" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="beagle_bathouse" data-author-fullname="t2_83rlaeff" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1euxho/" ><p class="parent"><a name="k1euxho"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/beagle_bathouse" class="author may-blank id-t2_83rlaeff" >beagle_bathouse</a><span class="userattrs"></span> <span class="score dislikes" title="3">3 points</span><span class="score unvoted" title="4">4 points</span><span class="score likes" title="5">5 points</span> <time title="Wed Sep 20 13:17:52 2023 UTC" datetime="2023-09-20T13:17:52+00:00" class="live-timestamp">5 months ago</time><time class="edited-timestamp" title="last edited 9 days ago" datetime="2024-02-09T18:57:46+00:00">*</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(1 child)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1euxhodgx"><input type="hidden" name="thing_id" value="t1_k1euxho"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>poor smell rhythm ink offend abounding person alive elderly dog</p>
+
+<p><em>This post was mass deleted and anonymized with <a href="https://redact.dev">Redact</a></em></p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1euxho/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1euxho/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1eqh9b" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1euxho"></div></div><div class="child"><div id="siteTable_t1_k1euxho" class="sitetable listing"><div class=" thing id-t1_k1gitjf noncollapsed   comment " id="thing_t1_k1gitjf" onclick="click_thing(this)" data-fullname="t1_k1gitjf" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="KidBeene" data-author-fullname="t2_bfou7" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1gitjf/" ><p class="parent"><a name="k1gitjf"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/KidBeene" class="author may-blank id-t2_bfou7" >KidBeene</a><span class="userattrs"></span> <span class="score dislikes" title="0">0 points</span><span class="score unvoted" title="1">1 point</span><span class="score likes" title="2">2 points</span> <time title="Wed Sep 20 19:17:57 2023 UTC" datetime="2023-09-20T19:17:57+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1gitjfetc"><input type="hidden" name="thing_id" value="t1_k1gitjf"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Yeah, the thing that is wrong is using twitter for industry news.</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1gitjf/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1gitjf/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="false" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li><a href="#k1euxho" data-event-action="parent" class="bylink" rel="nofollow" >parent</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1gitjf"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div></div></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1eu1yr noncollapsed   comment " id="thing_t1_k1eu1yr" onclick="click_thing(this)" data-fullname="t1_k1eu1yr" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="TulkasDeTX" data-author-fullname="t2_8wfrrqxs" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1eu1yr/" ><p class="parent"><a name="k1eu1yr"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/TulkasDeTX" class="author may-blank id-t2_8wfrrqxs" >TulkasDeTX</a><span class="userattrs"></span> <span class="score dislikes" title="0">0 points</span><span class="score unvoted" title="1">1 point</span><span class="score likes" title="2">2 points</span> <time title="Wed Sep 20 13:11:39 2023 UTC" datetime="2023-09-20T13:11:39+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1eu1yrkr4"><input type="hidden" name="thing_id" value="t1_k1eu1yr"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>I still get good infosec content, but yeah I'm basically for the same thing, where to go when troll-land finally goes down</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1eu1yr/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1eu1yr/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1eu1yr"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1fbj2l noncollapsed   comment " id="thing_t1_k1fbj2l" onclick="click_thing(this)" data-fullname="t1_k1fbj2l" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="True2this" data-author-fullname="t2_efgrr" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fbj2l/" ><p class="parent"><a name="k1fbj2l"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/True2this" class="author may-blank id-t2_efgrr" >True2this</a><span class="userattrs"></span> <span class="score dislikes" title="0">0 points</span><span class="score unvoted" title="1">1 point</span><span class="score likes" title="2">2 points</span> <time title="Wed Sep 20 15:05:35 2023 UTC" datetime="2023-09-20T15:05:35+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1fbj2l8uk"><input type="hidden" name="thing_id" value="t1_k1fbj2l"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Are you looking for just news feeds or something deeper? I use the open threat exchange from AlienVault. Good community - <a href="https://otx.alienvault.com" rel="nofollow">https://otx.alienvault.com</a></p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fbj2l/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1fbj2l/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1fbj2l"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1gzblu noncollapsed   comment " id="thing_t1_k1gzblu" onclick="click_thing(this)" data-fullname="t1_k1gzblu" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="manintheflask" data-author-fullname="t2_waj3n" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1gzblu/" ><p class="parent"><a name="k1gzblu"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/manintheflask" class="author may-blank id-t2_waj3n" >manintheflask</a><span class="userattrs"></span> <span class="score dislikes" title="0">0 points</span><span class="score unvoted" title="1">1 point</span><span class="score likes" title="2">2 points</span> <time title="Wed Sep 20 20:50:49 2023 UTC" datetime="2023-09-20T20:50:49+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1gzblu0hg"><input type="hidden" name="thing_id" value="t1_k1gzblu"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>I find this start[.]me URL pretty useful:<br/>
+<a href="https://start.me/p/wMrA5z/cyber-threat-intelligence" rel="nofollow">https://start.me/p/wMrA5z/cyber-threat-intelligence</a></p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1gzblu/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1gzblu/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1gzblu"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1h1jio noncollapsed   comment " id="thing_t1_k1h1jio" onclick="click_thing(this)" data-fullname="t1_k1h1jio" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="netbroom" data-author-fullname="t2_gcsmap2" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1h1jio/" ><p class="parent"><a name="k1h1jio"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/netbroom" class="author may-blank id-t2_gcsmap2" >netbroom</a><span class="userattrs"></span> <span class="score dislikes" title="0">0 points</span><span class="score unvoted" title="1">1 point</span><span class="score likes" title="2">2 points</span> <time title="Wed Sep 20 21:03:09 2023 UTC" datetime="2023-09-20T21:03:09+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1h1jios3b"><input type="hidden" name="thing_id" value="t1_k1h1jio"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Pulsedive has a free dashboard for infosec news</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1h1jio/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1h1jio/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1h1jio"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1j6zad noncollapsed   comment " id="thing_t1_k1j6zad" onclick="click_thing(this)" data-fullname="t1_k1j6zad" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="flusteredJonnies" data-author-fullname="t2_9ljkm" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1j6zad/" ><p class="parent"><a name="k1j6zad"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/flusteredJonnies" class="author may-blank id-t2_9ljkm" >flusteredJonnies</a><span class="userattrs"></span> <span class="score dislikes" title="0">0 points</span><span class="score unvoted" title="1">1 point</span><span class="score likes" title="2">2 points</span> <time title="Thu Sep 21 07:09:09 2023 UTC" datetime="2023-09-21T07:09:09+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1j6zad1ny"><input type="hidden" name="thing_id" value="t1_k1j6zad"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>Dude I was getting the WEIRDEST content on X before I had to delete it because it was absurd. I only follow infosec people. Like half of my timeline became fight videos randomly. Like videos of people fighting liveleak style. Stuff that was so violent I surely thought would violate some policy, but had TONS of engagement.</p>
+
+<p>Not sure what they changed over there but no matter how often I scrolled past or reported or did behaviors to show the algo I was not interested in the content, it was all over my TL for like a month. Deleted the app as it just insisted on pushing me weird or violent content. Bummed because it was a great news source for a while.</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1j6zad/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1j6zad/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1j6zad"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k1j868n noncollapsed   comment " id="thing_t1_k1j868n" onclick="click_thing(this)" data-fullname="t1_k1j868n" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="VAsHachiRoku" data-author-fullname="t2_8dcuztg" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1j868n/" ><p class="parent"><a name="k1j868n"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/VAsHachiRoku" class="author may-blank id-t2_8dcuztg" >VAsHachiRoku</a><span class="userattrs"></span> <span class="score dislikes" title="0">0 points</span><span class="score unvoted" title="1">1 point</span><span class="score likes" title="2">2 points</span> <time title="Thu Sep 21 07:23:59 2023 UTC" datetime="2023-09-21T07:23:59+00:00" class="live-timestamp">5 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k1j868nr5h"><input type="hidden" name="thing_id" value="t1_k1j868n"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p>We pay for threat intel company feeds like Mandiant, along with news and other information. Easier to have it come from a trusted source rather than many toxic places like X and Reddit. These both can draw in people with their own personal agendas and messages.</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1j868n/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k1j868n/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k1j868n"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div><div class=" thing id-t1_k27x83v noncollapsed   comment " id="thing_t1_k27x83v" onclick="click_thing(this)" data-fullname="t1_k27x83v" data-type="comment" data-gildings="0" data-subreddit="cybersecurity" data-subreddit-prefixed="r/cybersecurity" data-subreddit-fullname="t5_2u559" data-subreddit-type="public" data-author="Reshi-Snoo" data-author-fullname="t2_hj8duxb7" data-replies="0" data-permalink="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k27x83v/" ><p class="parent"><a name="k27x83v"></a></p><div class="midcol unvoted" ><div class="arrow up login-required access-required" data-event-action="upvote" role="button" aria-label="upvote" tabindex="0" ></div><div class="arrow down login-required access-required" data-event-action="downvote" role="button" aria-label="downvote" tabindex="0" ></div></div><div class="entry unvoted"><p class="tagline"><a href="javascript:void(0)" class="expand" onclick="return togglecomment(this)">[–]</a><a href="https://www.reddit.com/user/Reshi-Snoo" class="author may-blank id-t2_hj8duxb7" >Reshi-Snoo</a><span class="userattrs"></span> <span class="score dislikes" title="0">0 points</span><span class="score unvoted" title="1">1 point</span><span class="score likes" title="2">2 points</span> <time title="Tue Sep 26 01:01:08 2023 UTC" datetime="2023-09-26T01:01:08+00:00" class="live-timestamp">4 months ago</time> <a href="javascript:void(0)" class="numchildren" onclick="return togglecomment(this)">(0 children)</a></p><form action="#" class="usertext warn-on-unload" onsubmit="return post_form(this, 'editusertext')" id="form-t1_k27x83v9b2"><input type="hidden" name="thing_id" value="t1_k27x83v"/><div class="usertext-body may-blank-within md-container " ><div class="md"><p><a href="https://vulnu.mattjay.com" rel="nofollow">Vulnerable U</a></p>
+
+<p>Unsupervised learning</p>
+
+<p>Tl;drsec </p>
+
+<p>Are my favorite newsletters.</p>
+</div>
+</div></form><ul class="flat-list buttons"><li class="first"><a href="https://www.reddit.com/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k27x83v/" data-event-action="permalink" class="bylink" rel="nofollow" >permalink</a></li><li><a href="javascript:void(0)" data-comment="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/k27x83v/" data-media="www.redditmedia.com" data-link="/r/cybersecurity/comments/16nf2ev/twitterx_is_getting_weirder_where_now_for/" data-root="true" data-title="Twitter/X is getting weirder; where now for security news and analysis?" class="embed-comment" >embed</a></li><li class="comment-save-button save-button login-required"><a href="javascript:void(0)">save</a></li><li class="report-button login-required"><a href="javascript:void(0)" class="reportbtn access-required" data-event-action="report">report</a></li><li class="reply-button login-required"><a class="access-required" href="javascript:void(0)" data-event-action="comment" onclick="return reply(this)">reply</a></li></ul><div class="reportform report-t1_k27x83v"></div></div><div class="child"></div><div class="clearleft"></div></div><div class="clearleft"></div></div><script id="archived-popup" type="text/template"><div class="interstitial"><img class="interstitial-image" src="//www.redditstatic.com/interstitial-image-archived.png" alt="archived" height="150" width="150"><div class="interstitial-message md-container"><div class="md"><h3>This is an archived post. You won't be able to vote or comment.</h3><p>Posts are automatically archived after 6 months.</p></div></div><div class="buttons"><a href="/" class="c-btn c-btn-primary">Got It</a></div></div></script><script id="about-this-ad-popup" type="text/template"><h1 class="modal-title">About this ad</h1><div class="about-this-ad-body"></div><hr><div>Learn more about <a target="_blank" href="https://support.reddithelp.com/hc/en-us/articles/12731820767764-Control-the-ads-you-see-on-Reddit">controlling the ads you see on Reddit</a> or <a target="_blank" href="https://reddit.com/settings/privacy">manage your account settings.</a></div></script></div><script id="archived-popup" type="text/template"><div class="interstitial"><img class="interstitial-image" src="//www.redditstatic.com/interstitial-image-archived.png" alt="archived" height="150" width="150"><div class="interstitial-message md-container"><div class="md"><h3>This is an archived post. You won't be able to vote or comment.</h3><p>Posts are automatically archived after 6 months.</p></div></div><div class="buttons"><a href="/" class="c-btn c-btn-primary">Got It</a></div></div></script><script id="about-this-ad-popup" type="text/template"><h1 class="modal-title">About this ad</h1><div class="about-this-ad-body"></div><hr><div>Learn more about <a target="_blank" href="https://support.reddithelp.com/hc/en-us/articles/12731820767764-Control-the-ads-you-see-on-Reddit">controlling the ads you see on Reddit</a> or <a target="_blank" href="https://reddit.com/settings/privacy">manage your account settings.</a></div></script></div><div class="footer-parent"><div by-zero class="footer rounded"><div class="col"><ul class="flat-vert hover" ><li class="flat-vert title">about</li><li ><a href="https://redditblog.com" class="choice" >blog</a></li><li ><span class="separator"></span><a href="https://www.redditinc.com" class="choice" >about</a></li><li ><span class="separator"></span><a href="https://www.redditinc.com/advertising" class="choice" >advertising</a></li><li ><span class="separator"></span><a href="https://www.redditinc.com/careers" class="choice" >careers</a></li></ul></div><div class="col"><ul class="flat-vert hover" ><li class="flat-vert title">help</li><li ><a href="https://www.reddit.com/rules/" class="choice" >site rules</a></li><li ><span class="separator"></span><a href="https://www.reddithelp.com" class="choice" >Reddit help center</a></li><li ><span class="separator"></span><a href="https://www.reddit.com/wiki/reddiquette/" class="choice" >reddiquette</a></li><li ><span class="separator"></span><a href="https://www.reddit.com/help/healthycommunities/" class="choice" >mod guidelines</a></li><li ><span class="separator"></span><a href="https://www.reddit.com/contact/" class="choice" >contact us</a></li></ul></div><div class="col"><ul class="flat-vert hover" ><li class="flat-vert title">apps & tools</li><li ><a href="https://itunes.apple.com/us/app/reddit-the-official-app/id1064216828?mt=8" class="choice" >Reddit for iPhone</a></li><li ><span class="separator"></span><a href="https://play.google.com/store/apps/details?id=com.reddit.frontpage" class="choice" >Reddit for Android</a></li><li ><span class="separator"></span><a href="#" class="mweb-redirect-btn choice" >mobile website</a></li></ul></div><div class="col"><ul class="flat-vert hover" ><li class="flat-vert title"><3</li><li ><a href="https://www.reddit.com/premium/" class="buygold choice" >reddit premium</a></li></ul></div></div><p class="bottommenu">Use of this site constitutes acceptance of our <a href="https://www.reddit.com/help/useragreement" >User Agreement</a> and <a href="https://www.reddit.com/help/privacypolicy" >Privacy Policy</a>. © 2024 reddit inc. All rights reserved.</p><p class="bottommenu">REDDIT and the ALIEN Logo are registered trademarks of reddit inc.</p></div><script>var BETA_HOST = 'beta.reddit.com'; if (location.host === BETA_HOST) { r.config.https_endpoint = 'https://' + BETA_HOST; }</script><script id="login-popup" type="text/template"><!-- Login form function --><div id="desktop-onboarding-browse" class="c-step-sign-up"><div class="desktop-onboarding-step desktop-onboarding-step_sign-up"><div class="desktop-onboarding__col desktop-onboarding__col_sign-up_form"><div class="reddit-logo"><img width='200px' src="//www.redditstatic.com/logo.svg" /></div><h2 class="desktop-onboarding__title">Sign up to get your own personalized Reddit experience!</h2><p class="desktop-onboarding__description">By having a Reddit account, you can join, vote, and comment on all your favorite Reddit content. Sign up in just seconds.</p><div class="desktop-onboarding-sign-up__form-container c-is-create"><div class="desktop-onboarding-sign-up__form desktop-onboarding-sign-up__form_create"><h3 class="desktop-onboarding-sign-up__form-title">Enter email</h3><form class="sign-up-form" id="desktop-onboarding-sign-up-form" autocomplete="off"><div class="c-form-group "><label for="email" class="screenreader-only">email:</label><input name="email" id="desktop-onboarding-email" class="c-form-control" type="text" autofocus placeholder="email address" data-validate-url="/api/check_email.json" data-validate-on="keyup change blur" /><div class="c-form-control-feedback-wrapper "><span class="c-form-control-feedback c-form-control-feedback-throbber"></span><span class="c-form-control-feedback c-form-control-feedback-error" title=""></span><span class="c-form-control-feedback c-form-control-feedback-success"></span></div></div><button type="submit" class="c-btn c-btn-primary desktop-onboarding__next-button">Next</button><p class="desktop-onboarding-sign-up__form-note"><span>Already have an account?</span><a href="." class="desktop-onboarding-sign-up__form-toggler" data-form="login">Log In</a><a href="javascript: void 0;" class="skip-for-now">Skip for now</a></p></form></div><div class="desktop-onboarding-sign-up__form desktop-onboarding-sign-up__form_login"><h3 class="desktop-onboarding-sign-up__form-title">Log In</h3><form id="login-form" method="post" action="https://www.reddit.com/r/cybersecurity/post/login" class="form-v2 onboarding-login"><input type="hidden" name="op" value="login"><div class="c-form-group "><label for="user_login" class="screenreader-only">username</label><input value="" name="user" id="user_login" autofocus class="c-form-control" type="text" maxlength="20" tabindex="3" placeholder="username" ><div class="c-form-control-feedback-wrapper "><span class="c-form-control-feedback c-form-control-feedback-throbber"></span><span class="c-form-control-feedback c-form-control-feedback-error" title=""></span><span class="c-form-control-feedback c-form-control-feedback-success"></span></div></div><div class="c-form-group "><label for="passwd_login" class="screenreader-only">password</label><input id="passwd_login" class="c-form-control" name="passwd" type="password" tabindex="3" placeholder="password" ><div class="c-form-control-feedback-wrapper "><span class="c-form-control-feedback c-form-control-feedback-throbber"></span><span class="c-form-control-feedback c-form-control-feedback-error" title=""></span><span class="c-form-control-feedback c-form-control-feedback-success"></span></div></div><div class="desktop-onboarding-sign-up__form-note"><span>Don't have an account?</span><a href="." class="desktop-onboarding-sign-up__form-toggler" data-form="create">Sign up</a> |<a href="/password">Reset password</a></div><input type="hidden" value="yes" name="rem"/><div class="spacer"><div class="c-form-group g-recaptcha" data-sitekey="6LeTnxkTAAAAAN9QEuDZRpn90WwKk_R1TRW_g-JC"></div><span class="error BAD_CAPTCHA field-captcha" style="display:none"></span></div><div class="c-clearfix c-submit-group"><span class="c-form-throbber"></span><button type="submit" class="c-btn c-btn-primary c-pull-right" tabindex="3">log in</button></div><div><div class="c-alert c-alert-danger"></div><span class="status"></span></div></form></div></div><footer>By signing up, you agree to our <a href="https://www.reddit.com/help/useragreement/" >Terms</a> and that you have read our <a href="https://www.reddit.com/help/privacypolicy/" >Privacy Policy</a> and <a href="https://www.reddit.com/help/contentpolicy/" >Content Policy</a>.</footer></div><div class="desktop-onboarding__col desktop-onboarding__col_sign-up_image"></div></div><div class="desktop-onboarding-step desktop-onboarding-step_subreddit-picker"><div class="subreddit-picker-header"><h2 class="desktop-onboarding__title">Find the good stuff</h2><p class="desktop-onboarding__description">Reddit is filled with interest based communities, offering something for everyone. Check out some communities and we recommend you join at least 5.</p></div><div class="subreddit-picker"><ul class="subreddit-picker__categories"></ul><ul class="subreddit-picker__subreddits"></ul><div class="subreddit-picker__fail"><span>Something went wrong.</span><a href=".">Try Again?</a></div><div class="subreddit-picker__category-fail"><span>Something went wrong.</span><a href=".">Try Again?</a></div></div><footer><div class="subreddit-picker-progress"><div class="subreddit-picker-progress__track"><div class="subreddit-picker-progress__bar"></div></div><span class="subreddit-picker-progress__num">0</span><span>/</span><span class="subreddit-picker-progress__denom">5</span><span> <span class="subreddit-subscription-count">recommended communities</span></span></div><span class="desktop-onboarding__step-number">Step 2 of 3</span><div class="desktop-onboarding__buttons"><button class="c-btn desktop-onboarding__back-button">Back</button><button class="c-btn c-btn-primary desktop-onboarding__next-button">Next</button></div><div class="registration-error"></div></footer></div><div class="desktop-onboarding-step desktop-onboarding-step_username"><div class="desktop-onboarding__col desktop-onboarding__col_username_form"><h2 class="desktop-onboarding__title">Choose your username</h2><p class="desktop-onboarding__description">Your username is how other community members will see you. This name will be used to credit you for things you share on Reddit. What should we call you?</p><div class=desktop-onboarding-username-form><form id="register-form" method="post" action="https://www.reddit.com/r/cybersecurity/post/reg" autocomplete="off" class="form-v2 onboarding-login"><input type="hidden" name="op" value="reg"><input type="hidden" id="desktop-onboarding-register-email" name="email" value=""><input type="hidden" id="desktop-onboarding-subreddits" name="sr" value=""><div class="c-form-group "><label class="desktop-onboarding-sign-up__form-title" for="user_reg">Choose username</label><input value="" name="user" id="user_reg" autofocus class="c-form-control" type="text" maxlength="20" tabindex="2" placeholder="username" data-validate-url="/api/check_username.json" data-validate-min="3" autocomplete="new-username" ><div class="c-form-control-feedback-wrapper "><span class="c-form-control-feedback c-form-control-feedback-throbber"></span><span class="c-form-control-feedback c-form-control-feedback-error" title=""></span><span class="c-form-control-feedback c-form-control-feedback-success"></span></div></div><div class="c-form-group "><label for="passwd_reg" class="desktop-onboarding-sign-up__form-title">Set password</label><input id="passwd_reg" class="c-form-control" name="passwd" type="password" tabindex="2" placeholder="password" data-validate-url='/api/check_password.json' autocomplete='new-password'><div class="c-form-control-feedback-wrapper "><span class="c-form-control-feedback c-form-control-feedback-throbber"></span><span class="c-form-control-feedback c-form-control-feedback-error" title=""></span><span class="c-form-control-feedback c-form-control-feedback-success"></span></div></div><input type="hidden" name="passwd2" id="passwd2_reg" class="c-form-control"><input type="hidden" value="yes" name="rem"/><div class="spacer"><div class="c-form-group g-recaptcha" data-sitekey="6LeTnxkTAAAAAN9QEuDZRpn90WwKk_R1TRW_g-JC"></div><span class="error BAD_CAPTCHA field-captcha" style="display:none"></span></div><div><div class="c-alert c-alert-danger"></div><span class="status"></span><span class="error RATELIMIT field-ratelimit" style="display:none"></span><span class="error RATELIMIT field-vdelay" style="display:none"></span></div></form></div></div><div class="desktop-onboarding__col desktop-onboarding__col_username_picker"><div class="username-generator"><p class="desktop-onboarding__description">Having a hard time picking a name?<br />Here are some available suggestions.</p><div class="username-generator__suggestions"></div><a href="javascript: void 0;" class="username-generator__refresh-button">Refresh suggestions</a></div><footer><span class="desktop-onboarding__step-number">Step 3 of 3</span><div class="desktop-onboarding__buttons"><button class="c-btn desktop-onboarding__back-button">Back</button><button class="c-btn c-btn-primary desktop-onboarding__next-button">Submit</button></div></footer></div></div></div></script><script id="lang-popup" type="text/template"><form action="https://www.reddit.com/post/unlogged_options" method="post" id="pref-form" class="pretty-form short-text prefoptions"><input type="hidden" name="uh" value="" /><table class="content preftable"><tr><th>interface language</th><td class="prefright"><select id="lang" name="lang"><option selected='selected' value="en">English [en]</option><option value="af">Afrikaans [af] (*)</option><option value="ar">العربية [ar] (*)</option><option value="be">Беларуская мова [be] (*)</option><option value="bg">български език [bg]</option><option value="bn-IN">বাংলা [bn-IN] (*)</option><option value="bn-bd">বাংলা [bn-bd] (*)</option><option value="bs">Bosanski [bs] (*)</option><option value="ca">català [ca]</option><option value="cs">česky [cs]</option><option value="cy">Cymraeg [cy] (*)</option><option value="da">dansk [da]</option><option value="de">Deutsch [de]</option><option value="el">Ελληνικά [el]</option><option value="en-au">English (Australia) [en-au]</option><option value="en-ca">English (Canadian) [en-ca]</option><option value="en-gb">English (Great Britain) [en-gb]</option><option value="en-us">English [en-us]</option><option value="eo">Esperanto [eo] (*)</option><option value="es">español [es]</option><option value="es-ar">español [es-ar]</option><option value="es-cl">español [es-cl]</option><option value="es-mx">Español [es-mx]</option><option value="et">eesti keel [et] (*)</option><option value="eu">Euskara [eu]</option><option value="fa">فارسی [fa]</option><option value="fi">suomi [fi]</option><option value="fil">Filipino [fil] (*)</option><option value="fr">français [fr]</option><option value="fr-ca">Français [fr-ca]</option><option value="fy-NL">Frysk [fy-NL] (*)</option><option value="ga-ie">Gaeilge [ga-ie] (*)</option><option value="gd">Gàidhlig [gd]</option><option value="gl">Galego [gl] (*)</option><option value="he">עברית [he] (*)</option><option value="hi">मानक हिन्दी [hi] (*)</option><option value="hr">hrvatski [hr]</option><option value="hu">Magyar [hu]</option><option value="hy">Հայերեն լեզու [hy]</option><option value="id">Bahasa Indonesia [id] (*)</option><option value="is">íslenska [is]</option><option value="it">italiano (Italy) [it]</option><option value="ja">日本語 [ja]</option><option value="kn_IN">ಕನ್ನಡ [kn_IN]</option><option value="ko">한국어 [ko]</option><option value="la">Latin [la] (*)</option><option value="leet">1337 [leet]</option><option value="lol">LOL [lol]</option><option value="lt">lietuvių kalba [lt] (*)</option><option value="lv">latviešu valoda [lv]</option><option value="ms">Bahasa Melayu [ms] (*)</option><option value="mt-MT">Malti [mt-MT]</option><option value="nl">Nederlands [nl]</option><option value="nn">Nynorsk [nn]</option><option value="no">Norsk [no]</option><option value="pir">Arrrrrrrr! [pir] (*)</option><option value="pl">polski [pl]</option><option value="pt">português [pt] (*)</option><option value="pt-pt">português [pt-pt]</option><option value="pt_BR">português brasileiro [pt_BR]</option><option value="ro">română [ro]</option><option value="ru">русский [ru]</option><option value="sk">slovenčina [sk]</option><option value="sl">slovenščina [sl] (*)</option><option value="sr">српски језик [sr]</option><option value="sr-la">Srpski [sr-la]</option><option value="sv">Svenska [sv]</option><option value="ta">தமிழ் [ta]</option><option value="th">ภาษาไทย [th]</option><option value="tr">Türkçe [tr]</option><option value="uk">українська мова [uk]</option><option value="vi">Tiếng Việt [vi]</option><option value="zh">中文 [zh]</option><option value="zh-cn">简化字 [zh-cn]</option></select> <span class="details hover">(*) incomplete  <a href="https://www.reddit.com/r/i18n/wiki/getting_started">volunteer to translate</a></span></td></tr><tr><th>location</th><td class="preflight"><a href="https://www.reddit.com/settings/account/">set location preferences</a></td></tr><tr><td><input type="submit" class="btn save-preferences" value="save options"/></td></tr></table></form></script><img id="hsts_pixel" src="//reddit.com/static/pixel.png"><p class="debuginfo"><span class="icon">π</span> <span class="content">Rendered by PID 29 on  reddit-service-r2-slowlane-65c5c76ff5-v258h  at 2024-02-19 03:13:22.575220+00:00 running 5b0a0b2 country code: US.</span></p><script type="text/javascript" src="//www.redditstatic.com/reddit.en.lSSjgFdIksE.js"></script><script type="text/javascript" src="//www.redditstatic.com/spoiler-text.vsLMfxcst1g.js"></script><script type="text/javascript" src="//www.redditstatic.com/onetrust.6tPW2jUogoc.js"></script></body></html>
+\ No newline at end of file
diff --git a/test/fixtures/rich_media/yahoo.html b/test/fixtures/rich_media/yahoo.html
@@ -0,0 +1,12 @@
+<meta property="og:url" content="https://yahoo.com">
+<meta property="og:type" content="website">
+<meta property="og:title" content="Yahoo | Mail, Weather, Search, Politics, News, Finance, Sports & Videos">
+<meta property="og:description" content="Latest news coverage, email, free stock quotes, live scores and video are just the beginning. Discover more every day at Yahoo!">
+<meta property="og:image" content="https://s.yimg.com/cv/apiv2/social/images/yahoo_default_logo.png">
+
+<meta name="twitter:card" content="summary_large_image">
+<meta property="twitter:domain" content="yahoo.com">
+<meta property="twitter:url" content="https://yahoo.com">
+<meta name="twitter:title" content="Yahoo | Mail, Weather, Search, Politics, News, Finance, Sports & Videos">
+<meta name="twitter:description" content="Latest news coverage, email, free stock quotes, live scores and video are just the beginning. Discover more every day at Yahoo!">
+<meta name="twitter:image" content="https://s.yimg.com/cv/apiv2/social/images/yahoo_default_logo.png">
diff --git a/test/fixtures/tesla_mock/smithereen_non_anonymous_poll.json b/test/fixtures/tesla_mock/smithereen_non_anonymous_poll.json
@@ -0,0 +1 @@
+{"type":"Question","id":"https://friends.grishka.me/posts/54642","attributedTo":"https://friends.grishka.me/users/1","content":"<p>здесь тоже можно что-то написать отдельно от опроса</p>","published":"2021-09-04T00:22:16Z","url":"https://friends.grishka.me/posts/54642","to":["https://www.w3.org/ns/activitystreams#Public"],"cc":["https://friends.grishka.me/users/1/followers"],"replies":{"type":"Collection","id":"https://friends.grishka.me/posts/54642/replies","first":{"type":"CollectionPage","items":[],"partOf":"https://friends.grishka.me/posts/54642/replies","next":"https://friends.grishka.me/posts/54642/replies?page=1"}},"sensitive":false,"likes":"https://friends.grishka.me/posts/54642/likes","name":"тестовый опрос","oneOf":[{"type":"Note","id":"https://friends.grishka.me/posts/54642#options/76","name":"тестовый ответ 1","replies":{"type":"Collection","id":"https://friends.grishka.me/activitypub/objects/polls/24/options/76/votes","totalItems":4,"items":[]}},{"type":"Note","id":"https://friends.grishka.me/posts/54642#options/77","name":"тестовый ответ 2","replies":{"type":"Collection","id":"https://friends.grishka.me/activitypub/objects/polls/24/options/77/votes","totalItems":4,"items":[]}},{"type":"Note","id":"https://friends.grishka.me/posts/54642#options/78","name":"тестовый ответ 3","replies":{"type":"Collection","id":"https://friends.grishka.me/activitypub/objects/polls/24/options/78/votes","totalItems":6,"items":[]}}],"votersCount":14,"nonAnonymous":true,"@context":["https://www.w3.org/ns/activitystreams",{"sensitive":"as:sensitive","toot":"http://joinmastodon.org/ns#","sm":"http://smithereen.software/ns#","votersCount":"toot:votersCount","nonAnonymous":"sm:nonAnonymous"}]}
+\ No newline at end of file
diff --git a/test/fixtures/tesla_mock/smithereen_user.json b/test/fixtures/tesla_mock/smithereen_user.json
@@ -0,0 +1 @@
+{"type":"Person","id":"https://friends.grishka.me/users/1","name":"Григорий Клюшников","icon":{"type":"Image","image":{"type":"Image","url":"https://friends.grishka.me/i/6QLsOws97AWp5N_osd74C1IC1ijnFopyCBD9MSEeXNQ/q:93/bG9jYWw6Ly8vcy91cGxvYWRzL2F2YXRhcnMvNTYzODRhODEwODk5ZTRjMzI4YmY4YmQwM2Q2MWM3NmMud2VicA.jpg","mediaType":"image/jpeg","width":1280,"height":960},"width":573,"height":572,"cropRegion":[0.26422762870788574,0.3766937553882599,0.7113820910453796,0.9728997349739075],"url":"https://friends.grishka.me/i/ql_49PQcETAWgY_nC-Qj63H_Oa6FyOAEoWFkUSSkUvQ/c:573:572:nowe:338:362/q:93/bG9jYWw6Ly8vcy91cGxvYWRzL2F2YXRhcnMvNTYzODRhODEwODk5ZTRjMzI4YmY4YmQwM2Q2MWM3NmMud2VicA.jpg","mediaType":"image/jpeg"},"summary":"<p>Делаю эту хрень, пытаюсь вырвать социальные сети из жадных лап корпораций</p>\n<p></p>\n<p></p>\n<p></p>\n<p></p>\n<p></p>\n<p></p>\n<p></p>\n<p>This server does NOT support direct messages. Please write me <a href=\"https://t.me/grishka\">on Telegram</a> or <a href=\"https://matrix.to/#/@grishk:matrix.org\">Matrix</a>.</p>","url":"https://friends.grishka.me/grishka","preferredUsername":"grishka","inbox":"https://friends.grishka.me/users/1/inbox","outbox":"https://friends.grishka.me/users/1/outbox","followers":"https://friends.grishka.me/users/1/followers","following":"https://friends.grishka.me/users/1/following","endpoints":{"sharedInbox":"https://friends.grishka.me/activitypub/sharedInbox","collectionSimpleQuery":"https://friends.grishka.me/users/1/collectionQuery"},"publicKey":{"id":"https://friends.grishka.me/users/1#main-key","owner":"https://friends.grishka.me/users/1","publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjlakm+i/d9ER/hIeR7KfiFW+SdLZj2SkKIeM8cmR+YFJuh9ghFqXrkFEjcaqUnAFqe5gYDNSQACnDLA8y4DnzjfGNIohKAnRoa9x6GORmfKQvcnjaTZ53S1NvUiPPyc0Pv/vfCtY/Ab0CEXe5BLqL38oZn817Jf7pBrPRTYH7m012kvwAUTT6k0Y8lPITBEG7nzYbbuGcrN9Y/RDdwE08jmBXlZ45bahRH3VNXVpQE17dCzJB+7k+iJ1R7YCoI+DuMlBYGXGE2KVk46NZTuLnOjFV9SyXfWX4/SrJM4oxev+SX2N75tQgmNZmVVHeqg2ZcbC0WCfNjJOi2HHS9MujwIDAQAB\n-----END PUBLIC KEY-----\n"},"wall":"https://friends.grishka.me/users/1/wall","firstName":"Григорий","lastName":"Клюшников","middleName":"Александрович","vcard:bday":"1993-01-22","gender":"http://schema.org#Male","supportsFriendRequests":true,"friends":"https://friends.grishka.me/users/1/friends","groups":"https://friends.grishka.me/users/1/groups","capabilities":{"supportsFriendRequests":true},"@context":["https://www.w3.org/ns/activitystreams",{"sm":"http://smithereen.software/ns#","cropRegion":{"@id":"sm:cropRegion","@container":"@list"},"wall":{"@id":"sm:wall","@type":"@id"},"collectionSimpleQuery":"sm:collectionSimpleQuery","sc":"http://schema.org#","firstName":"sc:givenName","lastName":"sc:familyName","middleName":"sc:additionalName","gender":{"@id":"sc:gender","@type":"sc:GenderType"},"maidenName":"sm:maidenName","friends":{"@id":"sm:friends","@type":"@id"},"groups":{"@id":"sm:groups","@type":"@id"},"vcard":"http://www.w3.org/2006/vcard/ns#","capabilities":"litepub:capabilities","supportsFriendRequests":"sm:supportsFriendRequests","litepub":"http://litepub.social/ns#"},"https://w3id.org/security/v1"]}
+\ No newline at end of file
diff --git a/test/mix/pleroma_test.exs b/test/mix/pleroma_test.exs
@@ -39,7 +39,7 @@ defmodule Mix.PleromaTest do
describe "get_option/3" do
test "get from options" do
- assert get_option([domain: "some-domain.com"], :domain, "Promt") == "some-domain.com"
+ assert get_option([domain: "some-domain.com"], :domain, "Prompt") == "some-domain.com"
end
test "get from prompt" do
diff --git a/test/mix/tasks/pleroma/config_test.exs b/test/mix/tasks/pleroma/config_test.exs
@@ -140,7 +140,6 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do
federating: true,
federation_incoming_replies_max_depth: 100,
federation_reachability_timeout_days: 7,
- federation_publisher_modules: [Pleroma.Web.ActivityPub.Publisher],
allow_relay: true,
public: true,
quarantined_instances: [],
@@ -183,8 +182,8 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do
assert File.exists?(temp_file)
{:ok, file} = File.read(temp_file)
- assert file ==
- "import Config\n\nconfig :pleroma, :instance,\n name: \"Pleroma\",\n email: \"example@example.com\",\n notify_email: \"noreply@example.com\",\n description: \"A Pleroma instance, an alternative fediverse server\",\n limit: 5000,\n chat_limit: 5000,\n remote_limit: 100_000,\n upload_limit: 16_000_000,\n avatar_upload_limit: 2_000_000,\n background_upload_limit: 4_000_000,\n banner_upload_limit: 4_000_000,\n poll_limits: %{\n max_expiration: 31_536_000,\n max_option_chars: 200,\n max_options: 20,\n min_expiration: 0\n },\n registrations_open: true,\n federating: true,\n federation_incoming_replies_max_depth: 100,\n federation_reachability_timeout_days: 7,\n federation_publisher_modules: [Pleroma.Web.ActivityPub.Publisher],\n allow_relay: true,\n public: true,\n quarantined_instances: [],\n managed_config: true,\n static_dir: \"instance/static/\",\n allowed_post_formats: [\"text/plain\", \"text/html\", \"text/markdown\", \"text/bbcode\"],\n autofollowed_nicknames: [],\n max_pinned_statuses: 1,\n attachment_links: false,\n max_report_comment_size: 1000,\n safe_dm_mentions: false,\n healthcheck: false,\n remote_post_retention_days: 90,\n skip_thread_containment: true,\n limit_to_local_content: :unauthenticated,\n user_bio_length: 5000,\n user_name_length: 100,\n max_account_fields: 10,\n max_remote_account_fields: 20,\n account_field_name_length: 512,\n account_field_value_length: 2048,\n external_user_synchronization: true,\n extended_nickname_format: true,\n multi_factor_authentication: [\n totp: [digits: 6, period: 30],\n backup_codes: [number: 2, length: 6]\n ]\n"
+ assert file =~ "import Config\n"
+ assert file =~ "A Pleroma instance, an alternative fediverse server"
end
end
diff --git a/test/mix/tasks/pleroma/ecto/rollback_test.exs b/test/mix/tasks/pleroma/ecto/rollback_test.exs
@@ -13,7 +13,7 @@ defmodule Mix.Tasks.Pleroma.Ecto.RollbackTest do
assert capture_log(fn ->
Mix.Tasks.Pleroma.Ecto.Rollback.run(["--env", "test"])
- end) =~ "[info] Rollback succesfully"
+ end) =~ "[info] Rollback successfully"
Logger.configure(level: level)
end
diff --git a/test/mix/tasks/pleroma/robots_txt_test.exs b/test/mix/tasks/pleroma/robots_txt_test.exs
@@ -26,7 +26,7 @@ defmodule Mix.Tasks.Pleroma.RobotsTxtTest do
assert file == "User-Agent: *\nDisallow: /\n"
end
- test "to existance folder" do
+ test "to existing folder" do
path = "test/fixtures/"
file_path = path <> "robots.txt"
clear_config([:instance, :static_dir], path)
diff --git a/test/pleroma/activity_test.exs b/test/pleroma/activity_test.exs
@@ -145,6 +145,7 @@ defmodule Pleroma.ActivityTest do
setup do: clear_config([:instance, :limit_to_local_content])
+ @tag :skip_darwin
test "finds utf8 text in statuses", %{
japanese_activity: japanese_activity,
user: user
diff --git a/test/pleroma/bookmark_folder_test.exs b/test/pleroma/bookmark_folder_test.exs
@@ -0,0 +1,60 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.BookmarkFolderTest do
+ use Pleroma.DataCase, async: true
+ import Pleroma.Factory
+ alias Pleroma.BookmarkFolder
+
+ describe "create/3" do
+ test "with valid params" do
+ user = insert(:user)
+ {:ok, folder} = BookmarkFolder.create(user.id, "Read later", "🕓")
+ assert folder.user_id == user.id
+ assert folder.name == "Read later"
+ assert folder.emoji == "🕓"
+ end
+
+ test "with invalid params" do
+ {:error, changeset} = BookmarkFolder.create(nil, "", "not an emoji")
+ refute changeset.valid?
+
+ assert changeset.errors == [
+ emoji: {"Invalid emoji", []},
+ user_id: {"can't be blank", [validation: :required]},
+ name: {"can't be blank", [validation: :required]}
+ ]
+ end
+ end
+
+ test "update/3" do
+ user = insert(:user)
+ {:ok, folder} = BookmarkFolder.create(user.id, "Read ltaer")
+ {:ok, folder} = BookmarkFolder.update(folder.id, "Read later")
+ assert folder.name == "Read later"
+ end
+
+ test "for_user/1" do
+ user = insert(:user)
+ other_user = insert(:user)
+
+ {:ok, _} = BookmarkFolder.create(user.id, "Folder 1")
+ {:ok, _} = BookmarkFolder.create(user.id, "Folder 2")
+ {:ok, _} = BookmarkFolder.create(other_user.id, "Folder 3")
+
+ folders = BookmarkFolder.for_user(user.id)
+
+ assert length(folders) == 2
+ end
+
+ test "belongs_to_user?/2" do
+ user = insert(:user)
+ other_user = insert(:user)
+
+ {:ok, folder} = BookmarkFolder.create(user.id, "Folder")
+
+ assert true == BookmarkFolder.belongs_to_user?(folder.id, user.id)
+ assert false == BookmarkFolder.belongs_to_user?(folder.id, other_user.id)
+ end
+end
diff --git a/test/pleroma/bookmark_test.exs b/test/pleroma/bookmark_test.exs
@@ -6,15 +6,17 @@ defmodule Pleroma.BookmarkTest do
use Pleroma.DataCase, async: true
import Pleroma.Factory
alias Pleroma.Bookmark
+ alias Pleroma.BookmarkFolder
alias Pleroma.Web.CommonAPI
- describe "create/2" do
+ describe "create/3" do
test "with valid params" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "Some cool information"})
{:ok, bookmark} = Bookmark.create(user.id, activity.id)
assert bookmark.user_id == user.id
assert bookmark.activity_id == activity.id
+ assert bookmark.folder_id == nil
end
test "with invalid params" do
@@ -26,6 +28,19 @@ defmodule Pleroma.BookmarkTest do
activity_id: {"can't be blank", [validation: :required]}
]
end
+
+ test "update existing bookmark folder" do
+ user = insert(:user)
+ {:ok, activity} = CommonAPI.post(user, %{status: "Some cool information"})
+
+ {:ok, bookmark} = Bookmark.create(user.id, activity.id)
+ assert bookmark.folder_id == nil
+
+ {:ok, bookmark_folder} = BookmarkFolder.create(user.id, "Read later")
+
+ {:ok, bookmark} = Bookmark.create(user.id, activity.id, bookmark_folder.id)
+ assert bookmark.folder_id == bookmark_folder.id
+ end
end
describe "destroy/2" do
diff --git a/test/pleroma/config/deprecation_warnings_test.exs b/test/pleroma/config/deprecation_warnings_test.exs
@@ -125,13 +125,12 @@ defmodule Pleroma.Config.DeprecationWarningsTest do
media_removal: ["some.removal", {"some.other.instance", "Some reason"}]
)
- expected_config = [
+ expected_config =
{:media_removal, [{"some.removal", ""}, {"some.other.instance", "Some reason"}]}
- ]
capture_log(fn -> DeprecationWarnings.warn() end)
- assert Config.get([:mrf_simple]) == expected_config
+ assert expected_config in Config.get([:mrf_simple])
end
test "doesn't give a warning with correct config" do
@@ -215,7 +214,7 @@ defmodule Pleroma.Config.DeprecationWarningsTest do
```
config :pleroma, :mrf,
- transparency_exclusions: [{"instance.tld", "Reason to exlude transparency"}]
+ transparency_exclusions: [{"instance.tld", "Reason to exclude transparency"}]
```
"""
end
@@ -327,11 +326,11 @@ defmodule Pleroma.Config.DeprecationWarningsTest do
end) =~ "Your config is using old namespace for activity expiration configuration."
end
- test "check_uploders_s3_public_endpoint/0" do
+ test "check_uploaders_s3_public_endpoint/0" do
clear_config([Pleroma.Uploaders.S3], public_endpoint: "https://fake.amazonaws.com/bucket/")
assert capture_log(fn ->
- DeprecationWarnings.check_uploders_s3_public_endpoint()
+ DeprecationWarnings.check_uploaders_s3_public_endpoint()
end) =~
"Your config is using the old setting for controlling the URL of media uploaded to your S3 bucket."
end
diff --git a/test/pleroma/config_db_test.exs b/test/pleroma/config_db_test.exs
@@ -321,7 +321,7 @@ defmodule Pleroma.ConfigDBTest do
}) == {:proxy_url, {:socks5, {127, 0, 0, 1}, 1234}}
end
- test "tuple with n childs" do
+ test "tuple with n children" do
assert ConfigDB.to_elixir_types(%{
"tuple" => [
"v1",
@@ -399,7 +399,7 @@ defmodule Pleroma.ConfigDBTest do
assert ConfigDB.to_elixir_types(a: 1, b: 2, c: "string") == [a: 1, b: 2, c: "string"]
end
- test "complex keyword with nested mixed childs" do
+ test "complex keyword with nested mixed children" do
assert ConfigDB.to_elixir_types([
%{"tuple" => [":uploader", "Pleroma.Uploaders.Local"]},
%{"tuple" => [":filters", ["Pleroma.Upload.Filter.Dedupe"]]},
diff --git a/test/pleroma/conversation/participation_test.exs b/test/pleroma/conversation/participation_test.exs
@@ -57,7 +57,7 @@ defmodule Pleroma.Conversation.ParticipationTest do
assert Participation.unread_count(other_user) == 0
end
- test "for a new conversation, it sets the recipents of the participation" do
+ test "for a new conversation, it sets the recipients of the participation" do
user = insert(:user)
other_user = insert(:user)
third_user = insert(:user)
diff --git a/test/pleroma/emoji/loader_test.exs b/test/pleroma/emoji/loader_test.exs
@@ -72,7 +72,7 @@ defmodule Pleroma.Emoji.LoaderTest do
assert group == "special file"
end
- test "no mathing returns nil", %{groups: groups} do
+ test "no matching returns nil", %{groups: groups} do
group =
groups
|> Loader.match_extra("/emoji/some_undefined.png")
diff --git a/test/pleroma/emoji_test.exs b/test/pleroma/emoji_test.exs
@@ -6,26 +6,26 @@ defmodule Pleroma.EmojiTest do
use ExUnit.Case, async: true
alias Pleroma.Emoji
- describe "is_unicode_emoji?/1" do
+ describe "unicode?/1" do
test "tells if a string is an unicode emoji" do
- refute Emoji.is_unicode_emoji?("X")
- refute Emoji.is_unicode_emoji?("ね")
+ refute Emoji.unicode?("X")
+ refute Emoji.unicode?("ね")
# Only accept fully-qualified (RGI) emoji
# See http://www.unicode.org/reports/tr51/
- refute Emoji.is_unicode_emoji?("❤")
- refute Emoji.is_unicode_emoji?("☂")
+ refute Emoji.unicode?("❤")
+ refute Emoji.unicode?("☂")
- assert Emoji.is_unicode_emoji?("🥺")
- assert Emoji.is_unicode_emoji?("🤰")
- assert Emoji.is_unicode_emoji?("❤️")
- assert Emoji.is_unicode_emoji?("🏳️⚧️")
- assert Emoji.is_unicode_emoji?("🫵")
+ assert Emoji.unicode?("🥺")
+ assert Emoji.unicode?("🤰")
+ assert Emoji.unicode?("❤️")
+ assert Emoji.unicode?("🏳️⚧️")
+ assert Emoji.unicode?("🫵")
# Additionally, we accept regional indicators.
- assert Emoji.is_unicode_emoji?("🇵")
- assert Emoji.is_unicode_emoji?("🇴")
- assert Emoji.is_unicode_emoji?("🇬")
+ assert Emoji.unicode?("🇵")
+ assert Emoji.unicode?("🇴")
+ assert Emoji.unicode?("🇬")
end
end
diff --git a/test/pleroma/formatter_test.exs b/test/pleroma/formatter_test.exs
@@ -324,7 +324,7 @@ defmodule Pleroma.FormatterTest do
assert {_text, [], ^expected_tags} = Formatter.linkify(text)
end
- test "parses mulitple tags in html" do
+ test "parses multiple tags in html" do
text = "<p>#tag1 #tag2 #tag3 #tag4</p>"
expected_tags = [
@@ -347,7 +347,7 @@ defmodule Pleroma.FormatterTest do
assert {_text, [], ^expected_tags} = Formatter.linkify(text)
end
- test "parses mulitple tags on mulitple lines in html" do
+ test "parses multiple tags on multiple lines in html" do
text =
"<p>testing...</p><p>#tag1 #tag2 #tag3 #tag4</p><p>paragraph</p><p>#tag5 #tag6 #tag7 #tag8</p>"
diff --git a/test/pleroma/healthcheck_test.exs b/test/pleroma/healthcheck_test.exs
@@ -9,14 +9,16 @@ defmodule Pleroma.HealthcheckTest do
test "system_info/0" do
result = Healthcheck.system_info() |> Map.from_struct()
- assert Map.keys(result) == [
+ keys = Map.keys(result)
+
+ assert Keyword.equal?(keys, [
:active,
:healthy,
:idle,
:job_queue_stats,
:memory_used,
:pool_size
- ]
+ ])
end
describe "check_health/1" do
@@ -25,7 +27,7 @@ defmodule Pleroma.HealthcheckTest do
refute result.healthy
end
- test "chech_health/1" do
+ test "check_health/1" do
result = Healthcheck.check_health(%Healthcheck{pool_size: 10, active: 9})
assert result.healthy
end
diff --git a/test/pleroma/html_test.exs b/test/pleroma/html_test.exs
@@ -202,7 +202,7 @@ defmodule Pleroma.HTMLTest do
})
object = Object.normalize(activity, fetch: false)
- {:ok, url} = HTML.extract_first_external_url_from_object(object)
+ url = HTML.extract_first_external_url_from_object(object)
assert url == "https://github.com/komeiji-satori/Dress"
end
@@ -217,7 +217,7 @@ defmodule Pleroma.HTMLTest do
})
object = Object.normalize(activity, fetch: false)
- {:ok, url} = HTML.extract_first_external_url_from_object(object)
+ url = HTML.extract_first_external_url_from_object(object)
assert url == "https://github.com/syuilo/misskey/blob/develop/docs/setup.en.md"
@@ -233,7 +233,7 @@ defmodule Pleroma.HTMLTest do
})
object = Object.normalize(activity, fetch: false)
- {:ok, url} = HTML.extract_first_external_url_from_object(object)
+ url = HTML.extract_first_external_url_from_object(object)
assert url == "https://www.pixiv.net/member_illust.php?mode=medium&illust_id=72255140"
end
@@ -249,7 +249,7 @@ defmodule Pleroma.HTMLTest do
})
object = Object.normalize(activity, fetch: false)
- {:ok, url} = HTML.extract_first_external_url_from_object(object)
+ url = HTML.extract_first_external_url_from_object(object)
assert url == "https://www.pixiv.net/member_illust.php?mode=medium&illust_id=72255140"
end
@@ -261,7 +261,7 @@ defmodule Pleroma.HTMLTest do
object = Object.normalize(activity, fetch: false)
- assert {:ok, nil} = HTML.extract_first_external_url_from_object(object)
+ assert nil == HTML.extract_first_external_url_from_object(object)
end
test "skips attachment links" do
@@ -275,7 +275,7 @@ defmodule Pleroma.HTMLTest do
object = Object.normalize(activity, fetch: false)
- assert {:ok, nil} = HTML.extract_first_external_url_from_object(object)
+ assert nil == HTML.extract_first_external_url_from_object(object)
end
end
end
diff --git a/test/pleroma/http/adapter_helper/gun_test.exs b/test/pleroma/http/adapter_helper/gun_test.exs
@@ -36,7 +36,7 @@ defmodule Pleroma.HTTP.AdapterHelper.GunTest do
assert opts[:certificates_verification]
end
- test "https url with non standart port" do
+ test "https url with non-standard port" do
uri = URI.parse("https://example.com:115")
opts = Gun.options([receive_conn: false], uri)
@@ -44,7 +44,7 @@ defmodule Pleroma.HTTP.AdapterHelper.GunTest do
assert opts[:certificates_verification]
end
- test "merges with defaul http adapter config" do
+ test "merges with default http adapter config" do
defaults = Gun.options([receive_conn: false], URI.parse("https://example.com"))
assert Keyword.has_key?(defaults, :a)
assert Keyword.has_key?(defaults, :b)
diff --git a/test/pleroma/integration/mastodon_websocket_test.exs b/test/pleroma/integration/mastodon_websocket_test.exs
@@ -268,17 +268,6 @@ defmodule Pleroma.Integration.MastodonWebsocketTest do
end)
end
- test "accepts valid token on Sec-WebSocket-Protocol header", %{token: token} do
- assert {:ok, _} = start_socket("?stream=user", [{"Sec-WebSocket-Protocol", token.token}])
-
- capture_log(fn ->
- assert {:error, %WebSockex.RequestError{code: 401}} =
- start_socket("?stream=user", [{"Sec-WebSocket-Protocol", "I am a friend"}])
-
- Process.sleep(30)
- end)
- end
-
test "accepts valid token on client-sent event", %{token: token} do
assert {:ok, pid} = start_socket()
diff --git a/test/pleroma/maps_test.exs b/test/pleroma/maps_test.exs
@@ -0,0 +1,22 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2024 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.MapsTest do
+ use Pleroma.DataCase, async: true
+
+ alias Pleroma.Maps
+
+ describe "filter_empty_values/1" do
+ assert %{"bar" => "b", "ray" => ["foo"], "objs" => %{"a" => "b"}} ==
+ Maps.filter_empty_values(%{
+ "foo" => nil,
+ "fooz" => "",
+ "bar" => "b",
+ "rei" => [],
+ "ray" => ["foo"],
+ "obj" => %{},
+ "objs" => %{"a" => "b"}
+ })
+ end
+end
diff --git a/test/pleroma/mfa/totp_test.exs b/test/pleroma/mfa/totp_test.exs
@@ -7,6 +7,8 @@ defmodule Pleroma.MFA.TOTPTest do
alias Pleroma.MFA.TOTP
+ import Pleroma.Tests.Helpers, only: [uri_equal?: 2]
+
test "create provisioning_uri to generate qrcode" do
uri =
TOTP.provisioning_uri("test-secrcet", "test@example.com",
@@ -15,7 +17,9 @@ defmodule Pleroma.MFA.TOTPTest do
period: 60
)
- assert uri ==
+ assert uri_equal?(
+ uri,
"otpauth://totp/test@example.com?digits=8&issuer=Plerome-42&period=60&secret=test-secrcet"
+ )
end
end
diff --git a/test/pleroma/notification_test.exs b/test/pleroma/notification_test.exs
@@ -6,7 +6,6 @@ defmodule Pleroma.NotificationTest do
use Pleroma.DataCase, async: false
import Pleroma.Factory
- import Mock
alias Pleroma.FollowingRelationship
alias Pleroma.Notification
@@ -18,8 +17,6 @@ defmodule Pleroma.NotificationTest do
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.NotificationView
- alias Pleroma.Web.Push
- alias Pleroma.Web.Streamer
setup do
Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Config)
@@ -175,158 +172,7 @@ defmodule Pleroma.NotificationTest do
assert [user2.id, user3.id, user1.id] == Enum.map(notifications, & &1.user_id)
end
- describe "CommonApi.post/2 notification-related functionality" do
- test_with_mock "creates but does NOT send notification to blocker user",
- Push,
- [:passthrough],
- [] do
- user = insert(:user)
- blocker = insert(:user)
- {:ok, _user_relationship} = User.block(blocker, user)
-
- {:ok, _activity} = CommonAPI.post(user, %{status: "hey @#{blocker.nickname}!"})
-
- blocker_id = blocker.id
- assert [%Notification{user_id: ^blocker_id}] = Repo.all(Notification)
- refute called(Push.send(:_))
- end
-
- test_with_mock "creates but does NOT send notification to notification-muter user",
- Push,
- [:passthrough],
- [] do
- user = insert(:user)
- muter = insert(:user)
- {:ok, _user_relationships} = User.mute(muter, user)
-
- {:ok, _activity} = CommonAPI.post(user, %{status: "hey @#{muter.nickname}!"})
-
- muter_id = muter.id
- assert [%Notification{user_id: ^muter_id}] = Repo.all(Notification)
- refute called(Push.send(:_))
- end
-
- test_with_mock "creates but does NOT send notification to thread-muter user",
- Push,
- [:passthrough],
- [] do
- user = insert(:user)
- thread_muter = insert(:user)
-
- {:ok, activity} = CommonAPI.post(user, %{status: "hey @#{thread_muter.nickname}!"})
-
- {:ok, _} = CommonAPI.add_mute(thread_muter, activity)
-
- {:ok, _same_context_activity} =
- CommonAPI.post(user, %{
- status: "hey-hey-hey @#{thread_muter.nickname}!",
- in_reply_to_status_id: activity.id
- })
-
- [pre_mute_notification, post_mute_notification] =
- Repo.all(from(n in Notification, where: n.user_id == ^thread_muter.id, order_by: n.id))
-
- pre_mute_notification_id = pre_mute_notification.id
- post_mute_notification_id = post_mute_notification.id
-
- assert called(
- Push.send(
- :meck.is(fn
- %Notification{id: ^pre_mute_notification_id} -> true
- _ -> false
- end)
- )
- )
-
- refute called(
- Push.send(
- :meck.is(fn
- %Notification{id: ^post_mute_notification_id} -> true
- _ -> false
- end)
- )
- )
- end
- end
-
describe "create_notification" do
- @tag needs_streamer: true
- test "it creates a notification for user and send to the 'user' and the 'user:notification' stream" do
- %{user: user, token: oauth_token} = oauth_access(["read"])
-
- task =
- Task.async(fn ->
- {:ok, _topic} = Streamer.get_topic_and_add_socket("user", user, oauth_token)
- assert_receive {:render_with_user, _, _, _, _}, 4_000
- end)
-
- task_user_notification =
- Task.async(fn ->
- {:ok, _topic} =
- Streamer.get_topic_and_add_socket("user:notification", user, oauth_token)
-
- assert_receive {:render_with_user, _, _, _, _}, 4_000
- end)
-
- activity = insert(:note_activity)
-
- notify = Notification.create_notification(activity, user)
- assert notify.user_id == user.id
- Task.await(task)
- Task.await(task_user_notification)
- end
-
- test "it creates a notification for user if the user blocks the activity author" do
- activity = insert(:note_activity)
- author = User.get_cached_by_ap_id(activity.data["actor"])
- user = insert(:user)
- {:ok, _user_relationship} = User.block(user, author)
-
- assert Notification.create_notification(activity, user)
- end
-
- test "it creates a notification for the user if the user mutes the activity author" do
- muter = insert(:user)
- muted = insert(:user)
- {:ok, _} = User.mute(muter, muted)
- muter = Repo.get(User, muter.id)
- {:ok, activity} = CommonAPI.post(muted, %{status: "Hi @#{muter.nickname}"})
-
- notification = Notification.create_notification(activity, muter)
-
- assert notification.id
- assert notification.seen
- end
-
- test "notification created if user is muted without notifications" do
- muter = insert(:user)
- muted = insert(:user)
-
- {:ok, _user_relationships} = User.mute(muter, muted, %{notifications: false})
-
- {:ok, activity} = CommonAPI.post(muted, %{status: "Hi @#{muter.nickname}"})
-
- assert Notification.create_notification(activity, muter)
- end
-
- test "it creates a notification for an activity from a muted thread" do
- muter = insert(:user)
- other_user = insert(:user)
- {:ok, activity} = CommonAPI.post(muter, %{status: "hey"})
- CommonAPI.add_mute(muter, activity)
-
- {:ok, activity} =
- CommonAPI.post(other_user, %{
- status: "Hi @#{muter.nickname}",
- in_reply_to_status_id: activity.id
- })
-
- notification = Notification.create_notification(activity, muter)
-
- assert notification.id
- assert notification.seen
- end
-
test "it disables notifications from strangers" do
follower = insert(:user)
@@ -603,9 +449,7 @@ defmodule Pleroma.NotificationTest do
status: "hey yet again @#{other_user.nickname}!"
})
- [_, read_notification] = Notification.set_read_up_to(other_user, n2.id)
-
- assert read_notification.activity.object
+ Notification.set_read_up_to(other_user, n2.id)
[n3, n2, n1] = Notification.for_user(other_user)
@@ -680,7 +524,7 @@ defmodule Pleroma.NotificationTest do
status: "hey @#{other_user.nickname}!"
})
- {enabled_receivers, _disabled_receivers} = Notification.get_notified_from_activity(activity)
+ enabled_receivers = Notification.get_notified_from_activity(activity)
assert other_user in enabled_receivers
end
@@ -712,7 +556,7 @@ defmodule Pleroma.NotificationTest do
{:ok, activity} = Transmogrifier.handle_incoming(create_activity)
- {enabled_receivers, _disabled_receivers} = Notification.get_notified_from_activity(activity)
+ enabled_receivers = Notification.get_notified_from_activity(activity)
assert other_user in enabled_receivers
end
@@ -739,7 +583,7 @@ defmodule Pleroma.NotificationTest do
{:ok, activity} = Transmogrifier.handle_incoming(create_activity)
- {enabled_receivers, _disabled_receivers} = Notification.get_notified_from_activity(activity)
+ enabled_receivers = Notification.get_notified_from_activity(activity)
assert other_user not in enabled_receivers
end
@@ -756,8 +600,7 @@ defmodule Pleroma.NotificationTest do
{:ok, activity_two} = CommonAPI.favorite(third_user, activity_one.id)
- {enabled_receivers, _disabled_receivers} =
- Notification.get_notified_from_activity(activity_two)
+ enabled_receivers = Notification.get_notified_from_activity(activity_two)
assert other_user not in enabled_receivers
end
@@ -779,7 +622,7 @@ defmodule Pleroma.NotificationTest do
|> Map.put("to", [other_user.ap_id | like_data["to"]])
|> ActivityPub.persist(local: true)
- {enabled_receivers, _disabled_receivers} = Notification.get_notified_from_activity(like)
+ enabled_receivers = Notification.get_notified_from_activity(like)
assert other_user not in enabled_receivers
end
@@ -796,39 +639,36 @@ defmodule Pleroma.NotificationTest do
{:ok, activity_two} = CommonAPI.repeat(activity_one.id, third_user)
- {enabled_receivers, _disabled_receivers} =
- Notification.get_notified_from_activity(activity_two)
+ enabled_receivers = Notification.get_notified_from_activity(activity_two)
assert other_user not in enabled_receivers
end
- test "it returns blocking recipient in disabled recipients list" do
+ test "it does not return blocking recipient in recipients list" do
user = insert(:user)
other_user = insert(:user)
{:ok, _user_relationship} = User.block(other_user, user)
{:ok, activity} = CommonAPI.post(user, %{status: "hey @#{other_user.nickname}!"})
- {enabled_receivers, disabled_receivers} = Notification.get_notified_from_activity(activity)
+ enabled_receivers = Notification.get_notified_from_activity(activity)
assert [] == enabled_receivers
- assert [other_user] == disabled_receivers
end
- test "it returns notification-muting recipient in disabled recipients list" do
+ test "it does not return notification-muting recipient in recipients list" do
user = insert(:user)
other_user = insert(:user)
{:ok, _user_relationships} = User.mute(other_user, user)
{:ok, activity} = CommonAPI.post(user, %{status: "hey @#{other_user.nickname}!"})
- {enabled_receivers, disabled_receivers} = Notification.get_notified_from_activity(activity)
+ enabled_receivers = Notification.get_notified_from_activity(activity)
assert [] == enabled_receivers
- assert [other_user] == disabled_receivers
end
- test "it returns thread-muting recipient in disabled recipients list" do
+ test "it does not return thread-muting recipient in recipients list" do
user = insert(:user)
other_user = insert(:user)
@@ -842,14 +682,12 @@ defmodule Pleroma.NotificationTest do
in_reply_to_status_id: activity.id
})
- {enabled_receivers, disabled_receivers} =
- Notification.get_notified_from_activity(same_context_activity)
+ enabled_receivers = Notification.get_notified_from_activity(same_context_activity)
- assert [other_user] == disabled_receivers
refute other_user in enabled_receivers
end
- test "it returns non-following domain-blocking recipient in disabled recipients list" do
+ test "it does not return non-following domain-blocking recipient in recipients list" do
blocked_domain = "blocked.domain"
user = insert(:user, %{ap_id: "https://#{blocked_domain}/@actor"})
other_user = insert(:user)
@@ -858,10 +696,9 @@ defmodule Pleroma.NotificationTest do
{:ok, activity} = CommonAPI.post(user, %{status: "hey @#{other_user.nickname}!"})
- {enabled_receivers, disabled_receivers} = Notification.get_notified_from_activity(activity)
+ enabled_receivers = Notification.get_notified_from_activity(activity)
assert [] == enabled_receivers
- assert [other_user] == disabled_receivers
end
test "it returns following domain-blocking recipient in enabled recipients list" do
@@ -874,10 +711,9 @@ defmodule Pleroma.NotificationTest do
{:ok, activity} = CommonAPI.post(user, %{status: "hey @#{other_user.nickname}!"})
- {enabled_receivers, disabled_receivers} = Notification.get_notified_from_activity(activity)
+ enabled_receivers = Notification.get_notified_from_activity(activity)
assert [other_user] == enabled_receivers
- assert [] == disabled_receivers
end
test "it sends edited notifications to those who repeated a status" do
@@ -897,11 +733,10 @@ defmodule Pleroma.NotificationTest do
status: "hey @#{other_user.nickname}! mew mew"
})
- {enabled_receivers, _disabled_receivers} =
- Notification.get_notified_from_activity(edit_activity)
+ enabled_receivers = Notification.get_notified_from_activity(edit_activity)
assert repeated_user in enabled_receivers
- assert other_user not in enabled_receivers
+ refute other_user in enabled_receivers
end
end
@@ -1189,13 +1024,13 @@ defmodule Pleroma.NotificationTest do
assert Notification.for_user(user) == []
end
- test "it returns notifications from a muted user when with_muted is set", %{user: user} do
+ test "it doesn't return notifications from a muted user when with_muted is set", %{user: user} do
muted = insert(:user)
{:ok, _user_relationships} = User.mute(user, muted)
{:ok, _activity} = CommonAPI.post(muted, %{status: "hey @#{user.nickname}"})
- assert length(Notification.for_user(user, %{with_muted: true})) == 1
+ assert Enum.empty?(Notification.for_user(user, %{with_muted: true}))
end
test "it doesn't return notifications from a blocked user when with_muted is set", %{
diff --git a/test/pleroma/object/fetcher_test.exs b/test/pleroma/object/fetcher_test.exs
@@ -101,8 +101,7 @@ defmodule Pleroma.Object.FetcherTest do
test "it returns thread depth exceeded error if thread depth is exceeded" do
clear_config([:instance, :federation_incoming_replies_max_depth], 0)
- assert {:error, "Max thread distance exceeded."} =
- Fetcher.fetch_object_from_id(@ap_id, depth: 1)
+ assert {:error, :allowed_depth} = Fetcher.fetch_object_from_id(@ap_id, depth: 1)
end
test "it fetches object if max thread depth is restricted to 0 and depth is not specified" do
@@ -220,14 +219,14 @@ defmodule Pleroma.Object.FetcherTest do
end
test "handle HTTP 410 Gone response" do
- assert {:error, "Object has been deleted"} ==
+ assert {:error, :not_found} ==
Fetcher.fetch_and_contain_remote_object_from_id(
"https://mastodon.example.org/users/userisgone"
)
end
test "handle HTTP 404 response" do
- assert {:error, "Object has been deleted"} ==
+ assert {:error, :not_found} ==
Fetcher.fetch_and_contain_remote_object_from_id(
"https://mastodon.example.org/users/userisgone404"
)
diff --git a/test/pleroma/otp_version_test.exs b/test/pleroma/otp_version_test.exs
@@ -28,7 +28,7 @@ defmodule Pleroma.OTPVersionTest do
"23.0"
end
- test "with non existance file" do
+ test "with nonexistent file" do
assert OTPVersion.get_version_from_files([
"test/fixtures/warnings/otp_version/non-exising",
"test/fixtures/warnings/otp_version/22.4"
diff --git a/test/pleroma/repo/migrations/autolinker_to_linkify_test.exs b/test/pleroma/repo/migrations/autolinker_to_linkify_test.exs
@@ -29,13 +29,13 @@ defmodule Pleroma.Repo.Migrations.AutolinkerToLinkifyTest do
%{value: new_opts} = ConfigDB.get_by_params(%{group: :pleroma, key: Pleroma.Formatter})
- assert new_opts == [
+ assert Keyword.equal?(new_opts,
class: false,
extra: true,
new_window: false,
rel: "testing",
strip_prefix: false
- ]
+ )
clear_config(Pleroma.Formatter, new_opts)
assert new_opts == Pleroma.Config.get(Pleroma.Formatter)
@@ -67,6 +67,6 @@ defmodule Pleroma.Repo.Migrations.AutolinkerToLinkifyTest do
strip_prefix: false
]
- assert migration.transform_opts(old_opts) == expected_opts
+ assert Keyword.equal?(migration.transform_opts(old_opts), expected_opts)
end
end
diff --git a/test/pleroma/repo/migrations/fix_malformed_formatter_config_test.exs b/test/pleroma/repo/migrations/fix_malformed_formatter_config_test.exs
@@ -26,13 +26,13 @@ defmodule Pleroma.Repo.Migrations.FixMalformedFormatterConfigTest do
%{value: new_opts} = ConfigDB.get_by_params(%{group: :pleroma, key: Pleroma.Formatter})
- assert new_opts == [
+ assert Keyword.equal?(new_opts,
class: false,
extra: true,
new_window: false,
rel: "F",
strip_prefix: false
- ]
+ )
clear_config(Pleroma.Formatter, new_opts)
assert new_opts == Pleroma.Config.get(Pleroma.Formatter)
diff --git a/test/pleroma/reverse_proxy_test.exs b/test/pleroma/reverse_proxy_test.exs
@@ -306,7 +306,7 @@ defmodule Pleroma.ReverseProxyTest do
end
describe "response content disposition header" do
- test "not atachment", %{conn: conn} do
+ test "not attachment", %{conn: conn} do
disposition_headers_mock([
{"content-type", "image/gif"},
{"content-length", "0"}
diff --git a/test/pleroma/rule_test.exs b/test/pleroma/rule_test.exs
@@ -0,0 +1,57 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.RuleTest do
+ use Pleroma.DataCase, async: true
+
+ alias Pleroma.Repo
+ alias Pleroma.Rule
+
+ test "getting a list of rules sorted by priority" do
+ %{id: id1} = Rule.create(%{text: "Example rule"})
+ %{id: id2} = Rule.create(%{text: "Second rule", priority: 2})
+ %{id: id3} = Rule.create(%{text: "Third rule", priority: 1})
+
+ rules =
+ Rule.query()
+ |> Repo.all()
+
+ assert [%{id: ^id1}, %{id: ^id3}, %{id: ^id2}] = rules
+ end
+
+ test "creating rules" do
+ %{id: id} = Rule.create(%{text: "Example rule"})
+
+ assert %{text: "Example rule"} = Rule.get(id)
+ end
+
+ test "editing rules" do
+ %{id: id} = Rule.create(%{text: "Example rule"})
+
+ Rule.update(%{text: "There are no rules", priority: 2}, id)
+
+ assert %{text: "There are no rules", priority: 2} = Rule.get(id)
+ end
+
+ test "deleting rules" do
+ %{id: id} = Rule.create(%{text: "Example rule"})
+
+ Rule.delete(id)
+
+ assert [] =
+ Rule.query()
+ |> Pleroma.Repo.all()
+ end
+
+ test "getting rules by ids" do
+ %{id: id1} = Rule.create(%{text: "Example rule"})
+ %{id: id2} = Rule.create(%{text: "Second rule"})
+ %{id: _id3} = Rule.create(%{text: "Third rule"})
+
+ rules = Rule.get([id1, id2])
+
+ assert Enum.all?(rules, &(&1.id in [id1, id2]))
+ assert length(rules) == 2
+ end
+end
diff --git a/test/pleroma/search/database_search_test.exs b/test/pleroma/search/database_search_test.exs
@@ -35,21 +35,6 @@ defmodule Pleroma.Search.DatabaseSearchTest do
assert [] = Search.search(nil, "wednesday")
end
- test "using plainto_tsquery on postgres < 11" do
- old_version = :persistent_term.get({Pleroma.Repo, :postgres_version})
- :persistent_term.put({Pleroma.Repo, :postgres_version}, 10.0)
- on_exit(fn -> :persistent_term.put({Pleroma.Repo, :postgres_version}, old_version) end)
-
- user = insert(:user)
- {:ok, post} = CommonAPI.post(user, %{status: "it's wednesday my dudes"})
- {:ok, _post2} = CommonAPI.post(user, %{status: "it's wednesday my bros"})
-
- # plainto doesn't understand complex queries
- assert [result] = Search.search(nil, "wednesday -dudes")
-
- assert result.id == post.id
- end
-
test "using websearch_to_tsquery" do
user = insert(:user)
{:ok, _post} = CommonAPI.post(user, %{status: "it's wednesday my dudes"})
diff --git a/test/pleroma/signature_test.exs b/test/pleroma/signature_test.exs
@@ -113,7 +113,7 @@ defmodule Pleroma.SignatureTest do
test "it calls webfinger for 'acct:' accounts" do
with_mock(Pleroma.Web.WebFinger,
- finger: fn _ -> %{"ap_id" => "https://gensokyo.2hu/users/raymoo"} end
+ finger: fn _ -> {:ok, %{"ap_id" => "https://gensokyo.2hu/users/raymoo"}} end
) do
assert Signature.key_id_to_actor_id("acct:raymoo@gensokyo.2hu") ==
{:ok, "https://gensokyo.2hu/users/raymoo"}
diff --git a/test/pleroma/user/backup_test.exs b/test/pleroma/user/backup_test.exs
@@ -166,6 +166,7 @@ defmodule Pleroma.User.BackupTest do
test "it creates a zip archive with user data" do
user = insert(:user, %{nickname: "cofe", name: "Cofe", ap_id: "http://cofe.io/users/cofe"})
+ %{ap_id: other_ap_id} = other_user = insert(:user)
{:ok, %{object: %{data: %{"id" => id1}}} = status1} =
CommonAPI.post(user, %{status: "status1"})
@@ -182,6 +183,8 @@ defmodule Pleroma.User.BackupTest do
Bookmark.create(user.id, status2.id)
Bookmark.create(user.id, status3.id)
+ CommonAPI.follow(user, other_user)
+
assert {:ok, backup} = user |> Backup.new() |> Repo.insert()
assert {:ok, path} = Backup.export(backup, self())
assert {:ok, zipfile} = :zip.zip_open(String.to_charlist(path), [:memory])
@@ -261,6 +264,16 @@ defmodule Pleroma.User.BackupTest do
"type" => "OrderedCollection"
} = Jason.decode!(json)
+ assert {:ok, {'following.json', json}} = :zip.zip_get('following.json', zipfile)
+
+ assert %{
+ "@context" => "https://www.w3.org/ns/activitystreams",
+ "id" => "following.json",
+ "orderedItems" => [^other_ap_id],
+ "totalItems" => 1,
+ "type" => "OrderedCollection"
+ } = Jason.decode!(json)
+
:zip.zip_close(zipfile)
File.rm!(path)
end
diff --git a/test/pleroma/user_test.exs b/test/pleroma/user_test.exs
@@ -226,7 +226,7 @@ defmodule Pleroma.UserTest do
assert [] = User.get_follow_requests(followed)
end
- test "follow_all follows mutliple users" do
+ test "follow_all follows multiple users" do
user = insert(:user)
followed_zero = insert(:user)
followed_one = insert(:user)
@@ -250,7 +250,7 @@ defmodule Pleroma.UserTest do
refute User.following?(user, reverse_blocked)
end
- test "follow_all follows mutliple users without duplicating" do
+ test "follow_all follows multiple users without duplicating" do
user = insert(:user)
followed_zero = insert(:user)
followed_one = insert(:user)
@@ -873,7 +873,7 @@ defmodule Pleroma.UserTest do
end
end
- describe "get_or_fetch/1 remote users with tld, while BE is runned on subdomain" do
+ describe "get_or_fetch/1 remote users with tld, while BE is running on a subdomain" do
setup do: clear_config([Pleroma.Web.WebFinger, :update_nickname_on_user_fetch], true)
test "for mastodon" do
@@ -1018,13 +1018,13 @@ defmodule Pleroma.UserTest do
@tag capture_log: true
test "returns nil if no user could be fetched" do
- {:error, fetched_user} = User.get_or_fetch_by_nickname("nonexistant@social.heldscal.la")
- assert fetched_user == "not found nonexistant@social.heldscal.la"
+ {:error, fetched_user} = User.get_or_fetch_by_nickname("nonexistent@social.heldscal.la")
+ assert fetched_user == "not found nonexistent@social.heldscal.la"
end
- test "returns nil for nonexistant local user" do
- {:error, fetched_user} = User.get_or_fetch_by_nickname("nonexistant")
- assert fetched_user == "not found nonexistant"
+ test "returns nil for nonexistent local user" do
+ {:error, fetched_user} = User.get_or_fetch_by_nickname("nonexistent")
+ assert fetched_user == "not found nonexistent"
end
test "updates an existing user, if stale" do
@@ -1132,7 +1132,7 @@ defmodule Pleroma.UserTest do
assert cs.valid?
end
- test "it sets the follower_adress" do
+ test "it sets the follower_address" do
cs = User.remote_user_changeset(@valid_remote)
# remote users get a fake local follower address
assert cs.changes.follower_address ==
@@ -2424,20 +2424,20 @@ defmodule Pleroma.UserTest do
end
end
- describe "is_internal_user?/1" do
+ describe "internal?/1" do
test "non-internal user returns false" do
user = insert(:user)
- refute User.is_internal_user?(user)
+ refute User.internal?(user)
end
test "user with no nickname returns true" do
user = insert(:user, %{nickname: nil})
- assert User.is_internal_user?(user)
+ assert User.internal?(user)
end
test "user with internal-prefixed nickname returns true" do
user = insert(:user, %{nickname: "internal.test"})
- assert User.is_internal_user?(user)
+ assert User.internal?(user)
end
end
@@ -2683,13 +2683,23 @@ defmodule Pleroma.UserTest do
end
describe "full_nickname/1" do
- test "returns fully qualified nickname for local and remote users" do
- local_user =
- insert(:user, nickname: "local_user", ap_id: "https://somehost.com/users/local_user")
+ test "returns fully qualified nickname for local users" do
+ local_user = insert(:user, nickname: "local_user")
+ assert User.full_nickname(local_user) == "local_user@localhost"
+ end
+
+ test "returns fully qualified nickname for local users when using different domain for webfinger" do
+ clear_config([Pleroma.Web.WebFinger, :domain], "plemora.dev")
+
+ local_user = insert(:user, nickname: "local_user")
+
+ assert User.full_nickname(local_user) == "local_user@plemora.dev"
+ end
+
+ test "returns fully qualified nickname for remote users" do
remote_user = insert(:user, nickname: "remote@host.com", local: false)
- assert User.full_nickname(local_user) == "local_user@somehost.com"
assert User.full_nickname(remote_user) == "remote@host.com"
end
@@ -2884,6 +2894,20 @@ defmodule Pleroma.UserTest do
end
end
+ describe "get_familiar_followers/3" do
+ test "returns familiar followers for a pair of users" do
+ user1 = insert(:user)
+ %{id: id2} = user2 = insert(:user)
+ user3 = insert(:user)
+ _user4 = insert(:user)
+
+ User.follow(user1, user2)
+ User.follow(user2, user3)
+
+ assert [%{id: ^id2}] = User.get_familiar_followers(user3, user1)
+ end
+ end
+
describe "account endorsements" do
test "it pins people" do
user = insert(:user)
@@ -2918,4 +2942,51 @@ defmodule Pleroma.UserTest do
refute User.endorses?(user, pinned_user)
end
end
+
+ test "it checks fields links for a backlink" do
+ user = insert(:user, ap_id: "https://social.example.org/users/lain")
+
+ fields = [
+ %{"name" => "Link", "value" => "http://example.com/rel_me/null"},
+ %{"name" => "Verified link", "value" => "http://example.com/rel_me/link"},
+ %{"name" => "Not a link", "value" => "i'm not a link"}
+ ]
+
+ user
+ |> User.update_and_set_cache(%{raw_fields: fields})
+
+ ObanHelpers.perform_all()
+
+ user = User.get_cached_by_id(user.id)
+
+ assert [
+ %{"verified_at" => nil},
+ %{"verified_at" => verified_at},
+ %{"verified_at" => nil}
+ ] = user.fields
+
+ assert is_binary(verified_at)
+ end
+
+ test "updating fields does not invalidate previously validated links" do
+ user = insert(:user, ap_id: "https://social.example.org/users/lain")
+
+ user
+ |> User.update_and_set_cache(%{
+ raw_fields: [%{"name" => "verified link", "value" => "http://example.com/rel_me/link"}]
+ })
+
+ ObanHelpers.perform_all()
+
+ %User{fields: [%{"verified_at" => verified_at}]} = user = User.get_cached_by_id(user.id)
+
+ user
+ |> User.update_and_set_cache(%{
+ raw_fields: [%{"name" => "Verified link", "value" => "http://example.com/rel_me/link"}]
+ })
+
+ user = User.get_cached_by_id(user.id)
+
+ assert [%{"verified_at" => ^verified_at}] = user.fields
+ end
end
diff --git a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs
@@ -221,7 +221,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
user = insert(:user)
{:ok, post} = CommonAPI.post(user, %{status: "test", visibility: "local"})
- assert Pleroma.Web.ActivityPub.Visibility.is_local_public?(post)
+ assert Pleroma.Web.ActivityPub.Visibility.local_public?(post)
object = Object.normalize(post, fetch: false)
uuid = String.split(object.data["id"], "/") |> List.last()
@@ -238,7 +238,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
user = insert(:user)
{:ok, post} = CommonAPI.post(user, %{status: "test", visibility: "local"})
- assert Pleroma.Web.ActivityPub.Visibility.is_local_public?(post)
+ assert Pleroma.Web.ActivityPub.Visibility.local_public?(post)
object = Object.normalize(post, fetch: false)
uuid = String.split(object.data["id"], "/") |> List.last()
@@ -259,7 +259,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
{:ok, post} =
CommonAPI.post(user, %{status: "test @#{reader.nickname}", visibility: "local"})
- assert Pleroma.Web.ActivityPub.Visibility.is_local_public?(post)
+ assert Pleroma.Web.ActivityPub.Visibility.local_public?(post)
object = Object.normalize(post, fetch: false)
uuid = String.split(object.data["id"], "/") |> List.last()
@@ -436,7 +436,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
user = insert(:user)
{:ok, post} = CommonAPI.post(user, %{status: "test", visibility: "local"})
- assert Pleroma.Web.ActivityPub.Visibility.is_local_public?(post)
+ assert Pleroma.Web.ActivityPub.Visibility.local_public?(post)
uuid = String.split(post.data["id"], "/") |> List.last()
@@ -452,7 +452,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
user = insert(:user)
{:ok, post} = CommonAPI.post(user, %{status: "test", visibility: "local"})
- assert Pleroma.Web.ActivityPub.Visibility.is_local_public?(post)
+ assert Pleroma.Web.ActivityPub.Visibility.local_public?(post)
uuid = String.split(post.data["id"], "/") |> List.last()
@@ -895,6 +895,23 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
assert Activity.get_by_ap_id(data["id"])
end
+ test "it rejects an invalid incoming activity", %{conn: conn, data: data} do
+ user = insert(:user, is_active: false)
+
+ data =
+ data
+ |> Map.put("bcc", [user.ap_id])
+ |> Kernel.put_in(["object", "bcc"], [user.ap_id])
+
+ conn =
+ conn
+ |> assign(:valid_signature, true)
+ |> put_req_header("content-type", "application/activity+json")
+ |> post("/users/#{user.nickname}/inbox", data)
+
+ assert "Invalid request." == json_response(conn, 400)
+ end
+
test "it accepts messages with to as string instead of array", %{conn: conn, data: data} do
user = insert(:user)
diff --git a/test/pleroma/web/activity_pub/activity_pub_test.exs b/test/pleroma/web/activity_pub/activity_pub_test.exs
@@ -1028,7 +1028,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
refute repeat_activity in activities
end
- test "see your own posts even when they adress actors from blocked domains" do
+ test "see your own posts even when they address actors from blocked domains" do
user = insert(:user)
domain = "dogwhistle.zone"
diff --git a/test/pleroma/web/activity_pub/mrf/ensure_re_prepended_test.exs b/test/pleroma/web/activity_pub/mrf/ensure_re_prepended_test.exs
@@ -24,7 +24,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.EnsureRePrependedTest do
assert res["object"]["summary"] == "re: object-summary"
end
- test "it adds `re:` to summary object when child summary containts re-subject of parent summary " do
+ test "it adds `re:` to summary object when child summary contains re-subject of parent summary " do
message = %{
"type" => "Create",
"object" => %{
diff --git a/test/pleroma/web/activity_pub/mrf/force_mention_test.exs b/test/pleroma/web/activity_pub/mrf/force_mention_test.exs
@@ -0,0 +1,73 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.MRF.ForceMentionTest do
+ use Pleroma.DataCase
+ require Pleroma.Constants
+
+ alias Pleroma.Web.ActivityPub.MRF.ForceMention
+
+ import Pleroma.Factory
+
+ test "adds mention to a reply" do
+ lain =
+ insert(:user, ap_id: "https://lain.com/users/lain", nickname: "lain@lain.com", local: false)
+
+ niobleoum =
+ insert(:user,
+ ap_id: "https://www.minds.com/api/activitypub/users/1198929502760083472",
+ nickname: "niobleoum@minds.com",
+ local: false
+ )
+
+ status = File.read!("test/fixtures/minds-pleroma-mentioned-post.json") |> Jason.decode!()
+
+ status_activity = %{
+ "type" => "Create",
+ "actor" => lain.ap_id,
+ "object" => status
+ }
+
+ Pleroma.Web.ActivityPub.Transmogrifier.handle_incoming(status_activity)
+
+ reply = File.read!("test/fixtures/minds-invalid-mention-post.json") |> Jason.decode!()
+
+ reply_activity = %{
+ "type" => "Create",
+ "actor" => niobleoum.ap_id,
+ "object" => reply
+ }
+
+ {:ok, %{"object" => %{"tag" => tag}}} = ForceMention.filter(reply_activity)
+
+ assert Enum.find(tag, fn %{"href" => href} -> href == lain.ap_id end)
+ end
+
+ test "adds mention to a quote" do
+ user1 = insert(:user, ap_id: "https://misskey.io/users/83ssedkv53")
+ user2 = insert(:user, ap_id: "https://misskey.io/users/7rkrarq81i")
+
+ status = File.read!("test/fixtures/tesla_mock/misskey.io_8vs6wxufd0.json") |> Jason.decode!()
+
+ status_activity = %{
+ "type" => "Create",
+ "actor" => user1.ap_id,
+ "object" => status
+ }
+
+ Pleroma.Web.ActivityPub.Transmogrifier.handle_incoming(status_activity)
+
+ quote_post = File.read!("test/fixtures/quote_post/misskey_quote_post.json") |> Jason.decode!()
+
+ quote_activity = %{
+ "type" => "Create",
+ "actor" => user2.ap_id,
+ "object" => quote_post
+ }
+
+ {:ok, %{"object" => %{"tag" => tag}}} = ForceMention.filter(quote_activity)
+
+ assert Enum.find(tag, fn %{"href" => href} -> href == user1.ap_id end)
+ end
+end
diff --git a/test/pleroma/web/activity_pub/mrf/steal_emoji_policy_test.exs b/test/pleroma/web/activity_pub/mrf/steal_emoji_policy_test.exs
@@ -60,6 +60,59 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicyTest do
|> File.exists?()
end
+ test "works with unknown extension", %{path: path} do
+ message = %{
+ "type" => "Create",
+ "object" => %{
+ "emoji" => [{"firedfox", "https://example.org/emoji/firedfox"}],
+ "actor" => "https://example.org/users/admin"
+ }
+ }
+
+ fullpath = Path.join(path, "firedfox.png")
+
+ Tesla.Mock.mock(fn %{method: :get, url: "https://example.org/emoji/firedfox"} ->
+ %Tesla.Env{status: 200, body: File.read!("test/fixtures/image.jpg")}
+ end)
+
+ clear_config(:mrf_steal_emoji, hosts: ["example.org"], size_limit: 284_468)
+
+ refute "firedfox" in installed()
+ refute File.exists?(path)
+
+ assert {:ok, _message} = StealEmojiPolicy.filter(message)
+
+ assert "firedfox" in installed()
+ assert File.exists?(path)
+ assert File.exists?(fullpath)
+ end
+
+ test "rejects invalid shortcodes", %{path: path} do
+ message = %{
+ "type" => "Create",
+ "object" => %{
+ "emoji" => [{"fired/fox", "https://example.org/emoji/firedfox"}],
+ "actor" => "https://example.org/users/admin"
+ }
+ }
+
+ fullpath = Path.join(path, "fired/fox.png")
+
+ Tesla.Mock.mock(fn %{method: :get, url: "https://example.org/emoji/firedfox"} ->
+ %Tesla.Env{status: 200, body: File.read!("test/fixtures/image.jpg")}
+ end)
+
+ clear_config(:mrf_steal_emoji, hosts: ["example.org"], size_limit: 284_468)
+
+ refute "firedfox" in installed()
+ refute File.exists?(path)
+
+ assert {:ok, _message} = StealEmojiPolicy.filter(message)
+
+ refute "fired/fox" in installed()
+ refute File.exists?(fullpath)
+ end
+
test "reject regex shortcode", %{message: message} do
refute "firedfox" in installed()
diff --git a/test/pleroma/web/activity_pub/mrf_test.exs b/test/pleroma/web/activity_pub/mrf_test.exs
@@ -1,10 +1,13 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRFTest do
use ExUnit.Case
use Pleroma.Tests.Helpers
+
+ import ExUnit.CaptureLog
+
alias Pleroma.Web.ActivityPub.MRF
test "subdomains_regex/1" do
@@ -61,6 +64,14 @@ defmodule Pleroma.Web.ActivityPub.MRFTest do
refute MRF.subdomain_match?(regexes, "EXAMPLE.COM")
refute MRF.subdomain_match?(regexes, "example.com")
end
+
+ @tag capture_log: true
+ test "logs sensible error on accidental wildcard" do
+ assert_raise Regex.CompileError, fn ->
+ assert capture_log(MRF.subdomains_regex(["*unsafe.tld"])) =~
+ "MRF: Invalid subdomain Regex: *unsafe.tld"
+ end
+ end
end
describe "instance_list_from_tuples/1" do
diff --git a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs
@@ -93,6 +93,17 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidatorTest
%{valid?: true} = ArticleNotePageValidator.cast_and_validate(note)
end
+ test "a Note from Convergence AP Bridge validates" do
+ insert(:user, ap_id: "https://cc.mkdir.uk/ap/acct/hiira")
+
+ note =
+ "test/fixtures/ccworld-ap-bridge_note.json"
+ |> File.read!()
+ |> Jason.decode!()
+
+ %{valid?: true} = ArticleNotePageValidator.cast_and_validate(note)
+ end
+
test "a note with an attachment should work", _ do
insert(:user, %{ap_id: "https://owncast.localhost.localdomain/federation/user/streamer"})
diff --git a/test/pleroma/web/activity_pub/object_validators/attachment_validator_test.exs b/test/pleroma/web/activity_pub/object_validators/attachment_validator_test.exs
@@ -164,7 +164,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AttachmentValidatorTest do
assert attachment.mediaType == "image/jpeg"
end
- test "it transforms image dimentions to our internal format" do
+ test "it transforms image dimensions to our internal format" do
attachment = %{
"type" => "Document",
"name" => "Hello world",
diff --git a/test/pleroma/web/activity_pub/object_validators/chat_validation_test.exs b/test/pleroma/web/activity_pub/object_validators/chat_validation_test.exs
@@ -147,6 +147,21 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ChatValidationTest do
assert object["attachment"]
end
+ test "validates for a basic object with content but attachment set to empty array", %{
+ user: user,
+ recipient: recipient
+ } do
+ {:ok, valid_chat_message, _} = Builder.chat_message(user, recipient.ap_id, "Hello!")
+
+ valid_chat_message =
+ valid_chat_message
+ |> Map.put("attachment", [])
+
+ assert {:ok, object, _meta} = ObjectValidator.validate(valid_chat_message, [])
+
+ assert object == Map.drop(valid_chat_message, ["attachment"])
+ end
+
test "does not validate if the message has no content", %{
valid_chat_message: valid_chat_message
} do
diff --git a/test/pleroma/web/activity_pub/publisher_test.exs b/test/pleroma/web/activity_pub/publisher_test.exs
@@ -25,6 +25,17 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do
setup_all do: clear_config([:instance, :federating], true)
+ describe "should_federate?/1" do
+ test "it returns false when the inbox is nil" do
+ refute Publisher.should_federate?(nil, false)
+ refute Publisher.should_federate?(nil, true)
+ end
+
+ test "it returns true when public is true" do
+ assert Publisher.should_federate?(false, true)
+ end
+ end
+
describe "gather_webfinger_links/1" do
test "it returns links" do
user = insert(:user)
@@ -205,6 +216,7 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do
refute called(Instances.set_reachable(inbox))
end
+ @tag capture_log: true
test_with_mock "calls `Instances.set_unreachable` on target inbox on non-2xx HTTP response code",
Instances,
[:passthrough],
@@ -212,7 +224,8 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do
actor = insert(:user)
inbox = "http://404.site/users/nick1/inbox"
- assert {:error, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1})
+ assert {:discard, _} =
+ Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1})
assert called(Instances.set_unreachable(inbox))
end
@@ -268,7 +281,7 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do
describe "publish/2" do
test_with_mock "doesn't publish a non-public activity to quarantined instances.",
- Pleroma.Web.Federator.Publisher,
+ Pleroma.Web.ActivityPub.Publisher,
[:passthrough],
[] do
Config.put([:instance, :quarantined_instances], [{"domain.com", "some reason"}])
@@ -295,7 +308,7 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do
assert res == :ok
assert not called(
- Pleroma.Web.Federator.Publisher.enqueue_one(Publisher, %{
+ Publisher.enqueue_one(%{
inbox: "https://domain.com/users/nick1/inbox",
actor_id: actor.id,
id: note_activity.data["id"]
@@ -304,7 +317,7 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do
end
test_with_mock "Publishes a non-public activity to non-quarantined instances.",
- Pleroma.Web.Federator.Publisher,
+ Pleroma.Web.ActivityPub.Publisher,
[:passthrough],
[] do
Config.put([:instance, :quarantined_instances], [{"somedomain.com", "some reason"}])
@@ -331,8 +344,7 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do
assert res == :ok
assert called(
- Pleroma.Web.Federator.Publisher.enqueue_one(
- Publisher,
+ Publisher.enqueue_one(
%{
inbox: "https://domain.com/users/nick1/inbox",
actor_id: actor.id,
@@ -344,7 +356,7 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do
end
test_with_mock "Publishes to directly addressed actors with higher priority.",
- Pleroma.Web.Federator.Publisher,
+ Pleroma.Web.ActivityPub.Publisher,
[:passthrough],
[] do
note_activity = insert(:direct_note_activity)
@@ -356,8 +368,7 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do
assert res == :ok
assert called(
- Pleroma.Web.Federator.Publisher.enqueue_one(
- Publisher,
+ Publisher.enqueue_one(
%{
inbox: :_,
actor_id: actor.id,
@@ -369,7 +380,7 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do
end
test_with_mock "publishes an activity with BCC to all relevant peers.",
- Pleroma.Web.Federator.Publisher,
+ Pleroma.Web.ActivityPub.Publisher,
[:passthrough],
[] do
follower =
@@ -393,7 +404,7 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do
assert res == :ok
assert called(
- Pleroma.Web.Federator.Publisher.enqueue_one(Publisher, %{
+ Publisher.enqueue_one(%{
inbox: "https://domain.com/users/nick1/inbox",
actor_id: actor.id,
id: note_activity.data["id"]
@@ -402,7 +413,7 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do
end
test_with_mock "publishes a delete activity to peers who signed fetch requests to the create acitvity/object.",
- Pleroma.Web.Federator.Publisher,
+ Pleroma.Web.ActivityPub.Publisher,
[:passthrough],
[] do
fetcher =
@@ -443,8 +454,7 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do
assert res == :ok
assert called(
- Pleroma.Web.Federator.Publisher.enqueue_one(
- Publisher,
+ Publisher.enqueue_one(
%{
inbox: "https://domain.com/users/nick1/inbox",
actor_id: actor.id,
@@ -455,8 +465,7 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do
)
assert called(
- Pleroma.Web.Federator.Publisher.enqueue_one(
- Publisher,
+ Publisher.enqueue_one(
%{
inbox: "https://domain2.com/users/nick1/inbox",
actor_id: actor.id,
diff --git a/test/pleroma/web/activity_pub/side_effects_test.exs b/test/pleroma/web/activity_pub/side_effects_test.exs
@@ -17,11 +17,19 @@ defmodule Pleroma.Web.ActivityPub.SideEffectsTest do
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.SideEffects
+ alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.CommonAPI
+ alias Pleroma.Web.CommonAPI.ActivityDraft
import Mock
import Pleroma.Factory
+ defp get_announces_of_object(%{data: %{"id" => id}} = _object) do
+ Pleroma.Activity.Queries.by_type("Announce")
+ |> Pleroma.Activity.Queries.by_object_id(id)
+ |> Pleroma.Repo.all()
+ end
+
describe "handle_after_transaction" do
test "it streams out notifications and streams" do
author = insert(:user, local: true)
@@ -819,31 +827,6 @@ defmodule Pleroma.Web.ActivityPub.SideEffectsTest do
{:ok, announce, _} = SideEffects.handle(announce)
assert Repo.get_by(Notification, user_id: poster.id, activity_id: announce.id)
end
-
- test "it streams out the announce", %{announce: announce} do
- with_mocks([
- {
- Pleroma.Web.Streamer,
- [],
- [
- stream: fn _, _ -> nil end
- ]
- },
- {
- Pleroma.Web.Push,
- [],
- [
- send: fn _ -> nil end
- ]
- }
- ]) do
- {:ok, announce, _} = SideEffects.handle(announce)
-
- assert called(Pleroma.Web.Streamer.stream(["user", "list"], announce))
-
- assert called(Pleroma.Web.Push.send(:_))
- end
- end
end
describe "removing a follower" do
@@ -915,4 +898,85 @@ defmodule Pleroma.Web.ActivityPub.SideEffectsTest do
assert User.get_follow_state(user, followed, nil) == nil
end
end
+
+ describe "Group actors" do
+ setup do
+ poster =
+ insert(:user,
+ local: false,
+ nickname: "poster@example.com",
+ ap_id: "https://example.com/users/poster"
+ )
+
+ group = insert(:user, actor_type: "Group")
+
+ make_create = fn mentioned_users ->
+ mentions = mentioned_users |> Enum.map(fn u -> "@#{u.nickname}" end) |> Enum.join(" ")
+ {:ok, draft} = ActivityDraft.create(poster, %{status: "#{mentions} hey"})
+
+ create_activity_data =
+ Utils.make_create_data(draft.changes |> Map.put(:published, nil), %{})
+ |> put_in(["object", "id"], "https://example.com/object")
+ |> put_in(["id"], "https://example.com/activity")
+
+ assert Enum.all?(mentioned_users, fn u -> u.ap_id in create_activity_data["to"] end)
+
+ create_activity_data
+ end
+
+ %{poster: poster, group: group, make_create: make_create}
+ end
+
+ test "group should boost it", %{make_create: make_create, group: group} do
+ create_activity_data = make_create.([group])
+ {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false)
+
+ {:ok, _create_activity, _meta} =
+ SideEffects.handle(create_activity,
+ local: false,
+ object_data: create_activity_data["object"]
+ )
+
+ object = Object.normalize(create_activity, fetch: false)
+ assert [announce] = get_announces_of_object(object)
+ assert announce.actor == group.ap_id
+ end
+
+ test "remote group should not boost it", %{make_create: make_create, group: group} do
+ remote_group =
+ insert(:user, actor_type: "Group", local: false, nickname: "remotegroup@example.com")
+
+ create_activity_data = make_create.([group, remote_group])
+ {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false)
+
+ {:ok, _create_activity, _meta} =
+ SideEffects.handle(create_activity,
+ local: false,
+ object_data: create_activity_data["object"]
+ )
+
+ object = Object.normalize(create_activity, fetch: false)
+ assert [announce] = get_announces_of_object(object)
+ assert announce.actor == group.ap_id
+ end
+
+ test "group should not boost it if group is blocking poster", %{
+ make_create: make_create,
+ group: group,
+ poster: poster
+ } do
+ {:ok, _} = CommonAPI.block(group, poster)
+ create_activity_data = make_create.([group])
+ {:ok, create_activity, _meta} = ActivityPub.persist(create_activity_data, local: false)
+
+ {:ok, _create_activity, _meta} =
+ SideEffects.handle(create_activity,
+ local: false,
+ object_data: create_activity_data["object"]
+ )
+
+ object = Object.normalize(create_activity, fetch: false)
+ assert [] = get_announces_of_object(object)
+ end
+ end
end
diff --git a/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs
@@ -221,6 +221,19 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.NoteHandlingTest do
"<p><span class=\"h-card\"><a href=\"http://localtesting.pleroma.lol/users/lain\" class=\"u-url mention\">@<span>lain</span></a></span></p>"
end
+ test "it works for incoming notices with a nil contentMap (firefish)" do
+ data =
+ File.read!("test/fixtures/mastodon-post-activity-contentmap.json")
+ |> Jason.decode!()
+ |> Map.put("contentMap", nil)
+
+ {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
+ object = Object.normalize(data["object"], fetch: false)
+
+ assert object.data["content"] ==
+ "<p><span class=\"h-card\"><a href=\"http://localtesting.pleroma.lol/users/lain\" class=\"u-url mention\">@<span>lain</span></a></span></p>"
+ end
+
test "it works for incoming notices with to/cc not being an array (kroeg)" do
data = File.read!("test/fixtures/kroeg-post-activity.json") |> Jason.decode!()
@@ -508,7 +521,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.NoteHandlingTest do
[data: data]
end
- test "returns not modified object when hasn't containts inReplyTo field", %{data: data} do
+ test "returns not modified object when has no inReplyTo field", %{data: data} do
assert Transmogrifier.fix_in_reply_to(data) == data
end
diff --git a/test/pleroma/web/activity_pub/transmogrifier/undo_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/undo_handling_test.exs
@@ -32,7 +32,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.UndoHandlingTest do
assert activity.data["type"] == "Undo"
end
- test "it returns an error for incoming unlikes wihout a like activity" do
+ test "it returns an error for incoming unlikes without a like activity" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "leave a like pls"})
diff --git a/test/pleroma/web/activity_pub/transmogrifier_test.exs b/test/pleroma/web/activity_pub/transmogrifier_test.exs
@@ -132,7 +132,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
assert {:ok, activity} = Transmogrifier.handle_incoming(message)
object = Object.normalize(activity)
assert [%{"type" => "Mention"}, %{"type" => "Link"}] = object.data["tag"]
- end) =~ "Error while fetching"
+ end) =~ "Object rejected while fetching"
end
test "it accepts quote posts" do
@@ -410,7 +410,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
assert capture_log(fn ->
{:error, _} = Transmogrifier.handle_incoming(data)
- end) =~ "Object containment failed"
+ end) =~ "Object rejected while fetching"
end
test "it rejects activities which reference objects that have an incorrect attribution (variant 1)" do
@@ -425,7 +425,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
assert capture_log(fn ->
{:error, _} = Transmogrifier.handle_incoming(data)
- end) =~ "Object containment failed"
+ end) =~ "Object rejected while fetching"
end
test "it rejects activities which reference objects that have an incorrect attribution (variant 2)" do
@@ -440,7 +440,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
assert capture_log(fn ->
{:error, _} = Transmogrifier.handle_incoming(data)
- end) =~ "Object containment failed"
+ end) =~ "Object rejected while fetching"
end
end
diff --git a/test/pleroma/web/activity_pub/utils_test.exs b/test/pleroma/web/activity_pub/utils_test.exs
@@ -17,7 +17,7 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do
require Pleroma.Constants
describe "strip_report_status_data/1" do
- test "does not break on issues with the reported activites" do
+ test "does not break on issues with the reported activities" do
reporter = insert(:user)
target_account = insert(:user)
{:ok, activity} = CommonAPI.post(target_account, %{status: "foobar"})
@@ -153,7 +153,7 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do
assert Enum.sort(cc) == expected_cc
end
- test "does not adress actor's follower address if the activity is not public", %{
+ test "does not address actor's follower address if the activity is not public", %{
user: user,
other_user: other_user,
third_user: third_user
@@ -622,7 +622,7 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do
end
describe "get_cached_emoji_reactions/1" do
- test "returns the normalized data or an emtpy list" do
+ test "returns the normalized data or an empty list" do
object = insert(:note)
assert Utils.get_cached_emoji_reactions(object) == []
diff --git a/test/pleroma/web/activity_pub/views/user_view_test.exs b/test/pleroma/web/activity_pub/views/user_view_test.exs
@@ -91,6 +91,13 @@ defmodule Pleroma.Web.ActivityPub.UserViewTest do
assert %{"alsoKnownAs" => ^akas} = UserView.render("user.json", %{user: user})
end
+ test "renders full nickname" do
+ clear_config([Pleroma.Web.WebFinger, :domain], "plemora.dev")
+
+ user = insert(:user, nickname: "user")
+ assert %{"webfinger" => "acct:user@plemora.dev"} = UserView.render("user.json", %{user: user})
+ end
+
describe "endpoints" do
test "local users have a usable endpoints structure" do
user = insert(:user)
diff --git a/test/pleroma/web/activity_pub/visibility_test.exs b/test/pleroma/web/activity_pub/visibility_test.exs
@@ -52,60 +52,60 @@ defmodule Pleroma.Web.ActivityPub.VisibilityTest do
}
end
- test "is_direct?", %{
+ test "direct?", %{
public: public,
private: private,
direct: direct,
unlisted: unlisted,
list: list
} do
- assert Visibility.is_direct?(direct)
- refute Visibility.is_direct?(public)
- refute Visibility.is_direct?(private)
- refute Visibility.is_direct?(unlisted)
- assert Visibility.is_direct?(list)
+ assert Visibility.direct?(direct)
+ refute Visibility.direct?(public)
+ refute Visibility.direct?(private)
+ refute Visibility.direct?(unlisted)
+ assert Visibility.direct?(list)
end
- test "is_public?", %{
+ test "public?", %{
public: public,
private: private,
direct: direct,
unlisted: unlisted,
list: list
} do
- refute Visibility.is_public?(direct)
- assert Visibility.is_public?(public)
- refute Visibility.is_public?(private)
- assert Visibility.is_public?(unlisted)
- refute Visibility.is_public?(list)
+ refute Visibility.public?(direct)
+ assert Visibility.public?(public)
+ refute Visibility.public?(private)
+ assert Visibility.public?(unlisted)
+ refute Visibility.public?(list)
end
- test "is_private?", %{
+ test "private?", %{
public: public,
private: private,
direct: direct,
unlisted: unlisted,
list: list
} do
- refute Visibility.is_private?(direct)
- refute Visibility.is_private?(public)
- assert Visibility.is_private?(private)
- refute Visibility.is_private?(unlisted)
- refute Visibility.is_private?(list)
+ refute Visibility.private?(direct)
+ refute Visibility.private?(public)
+ assert Visibility.private?(private)
+ refute Visibility.private?(unlisted)
+ refute Visibility.private?(list)
end
- test "is_list?", %{
+ test "list?", %{
public: public,
private: private,
direct: direct,
unlisted: unlisted,
list: list
} do
- refute Visibility.is_list?(direct)
- refute Visibility.is_list?(public)
- refute Visibility.is_list?(private)
- refute Visibility.is_list?(unlisted)
- assert Visibility.is_list?(list)
+ refute Visibility.list?(direct)
+ refute Visibility.list?(public)
+ refute Visibility.list?(private)
+ refute Visibility.list?(unlisted)
+ assert Visibility.list?(list)
end
test "visible_for_user? Activity", %{
@@ -227,7 +227,7 @@ defmodule Pleroma.Web.ActivityPub.VisibilityTest do
} do
Repo.delete(user)
Pleroma.User.invalidate_cache(user)
- refute Visibility.is_private?(direct)
+ refute Visibility.private?(direct)
end
test "get_visibility", %{
diff --git a/test/pleroma/web/admin_api/controllers/config_controller_test.exs b/test/pleroma/web/admin_api/controllers/config_controller_test.exs
@@ -873,7 +873,7 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
%{
"tuple" => [
":_",
- "Phoenix.Endpoint.Cowboy2Handler",
+ "Plug.Cowboy.Handler",
%{"tuple" => ["Pleroma.Web.Endpoint", []]}
]
}
@@ -937,7 +937,7 @@ defmodule Pleroma.Web.AdminAPI.ConfigControllerTest do
%{
"tuple" => [
":_",
- "Phoenix.Endpoint.Cowboy2Handler",
+ "Plug.Cowboy.Handler",
%{"tuple" => ["Pleroma.Web.Endpoint", []]}
]
}
diff --git a/test/pleroma/web/admin_api/controllers/o_auth_app_controller_test.exs b/test/pleroma/web/admin_api/controllers/o_auth_app_controller_test.exs
@@ -163,7 +163,7 @@ defmodule Pleroma.Web.AdminAPI.OAuthAppControllerTest do
assert response == ""
end
- test "with non existance id", %{conn: conn} do
+ test "with nonexistent id", %{conn: conn} do
response =
conn
|> delete("/api/pleroma/admin/oauth_app/0")
diff --git a/test/pleroma/web/admin_api/controllers/report_controller_test.exs b/test/pleroma/web/admin_api/controllers/report_controller_test.exs
@@ -11,6 +11,7 @@ defmodule Pleroma.Web.AdminAPI.ReportControllerTest do
alias Pleroma.ModerationLog
alias Pleroma.Repo
alias Pleroma.ReportNote
+ alias Pleroma.Rule
alias Pleroma.Web.CommonAPI
setup do
@@ -436,6 +437,34 @@ defmodule Pleroma.Web.AdminAPI.ReportControllerTest do
"error" => "Invalid credentials."
}
end
+
+ test "returns reports with specified role_id", %{conn: conn} do
+ [reporter, target_user] = insert_pair(:user)
+
+ %{id: rule_id} = Rule.create(%{text: "Example rule"})
+
+ rule_id = to_string(rule_id)
+
+ {:ok, %{id: report_id}} =
+ CommonAPI.report(reporter, %{
+ account_id: target_user.id,
+ comment: "",
+ rule_ids: [rule_id]
+ })
+
+ {:ok, _report} =
+ CommonAPI.report(reporter, %{
+ account_id: target_user.id,
+ comment: ""
+ })
+
+ response =
+ conn
+ |> get("/api/pleroma/admin/reports?rule_id=#{rule_id}")
+ |> json_response_and_validate_schema(:ok)
+
+ assert %{"reports" => [%{"id" => ^report_id}]} = response
+ end
end
describe "POST /api/pleroma/admin/reports/:id/notes" do
diff --git a/test/pleroma/web/admin_api/controllers/rule_controller_test.exs b/test/pleroma/web/admin_api/controllers/rule_controller_test.exs
@@ -0,0 +1,82 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.AdminAPI.RuleControllerTest do
+ use Pleroma.Web.ConnCase, async: true
+
+ import Pleroma.Factory
+
+ alias Pleroma.Rule
+
+ setup do
+ admin = insert(:user, is_admin: true)
+ token = insert(:oauth_admin_token, user: admin)
+
+ conn =
+ build_conn()
+ |> assign(:user, admin)
+ |> assign(:token, token)
+
+ {:ok, %{admin: admin, token: token, conn: conn}}
+ end
+
+ describe "GET /api/pleroma/admin/rules" do
+ test "sorts rules by priority", %{conn: conn} do
+ %{id: id1} = Rule.create(%{text: "Example rule"})
+ %{id: id2} = Rule.create(%{text: "Second rule", priority: 2})
+ %{id: id3} = Rule.create(%{text: "Third rule", priority: 1})
+
+ id1 = to_string(id1)
+ id2 = to_string(id2)
+ id3 = to_string(id3)
+
+ response =
+ conn
+ |> get("/api/pleroma/admin/rules")
+ |> json_response_and_validate_schema(:ok)
+
+ assert [%{"id" => ^id1}, %{"id" => ^id3}, %{"id" => ^id2}] = response
+ end
+ end
+
+ describe "POST /api/pleroma/admin/rules" do
+ test "creates a rule", %{conn: conn} do
+ %{"id" => id} =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> post("/api/pleroma/admin/rules", %{text: "Example rule"})
+ |> json_response_and_validate_schema(:ok)
+
+ assert %{text: "Example rule"} = Rule.get(id)
+ end
+ end
+
+ describe "PATCH /api/pleroma/admin/rules" do
+ test "edits a rule", %{conn: conn} do
+ %{id: id} = Rule.create(%{text: "Example rule"})
+
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> patch("/api/pleroma/admin/rules/#{id}", %{text: "There are no rules", priority: 2})
+ |> json_response_and_validate_schema(:ok)
+
+ assert %{text: "There are no rules", priority: 2} = Rule.get(id)
+ end
+ end
+
+ describe "DELETE /api/pleroma/admin/rules" do
+ test "deletes a rule", %{conn: conn} do
+ %{id: id} = Rule.create(%{text: "Example rule"})
+
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> delete("/api/pleroma/admin/rules/#{id}")
+ |> json_response_and_validate_schema(:ok)
+
+ assert [] =
+ Rule.query()
+ |> Pleroma.Repo.all()
+ end
+ end
+end
diff --git a/test/pleroma/web/admin_api/views/report_view_test.exs b/test/pleroma/web/admin_api/views/report_view_test.exs
@@ -7,6 +7,7 @@ defmodule Pleroma.Web.AdminAPI.ReportViewTest do
import Pleroma.Factory
+ alias Pleroma.Rule
alias Pleroma.Web.AdminAPI
alias Pleroma.Web.AdminAPI.Report
alias Pleroma.Web.AdminAPI.ReportView
@@ -38,7 +39,8 @@ defmodule Pleroma.Web.AdminAPI.ReportViewTest do
statuses: [],
notes: [],
state: "open",
- id: activity.id
+ id: activity.id,
+ rules: []
}
result =
@@ -76,7 +78,8 @@ defmodule Pleroma.Web.AdminAPI.ReportViewTest do
statuses: [StatusView.render("show.json", %{activity: activity})],
state: "open",
notes: [],
- id: report_activity.id
+ id: report_activity.id,
+ rules: []
}
result =
@@ -168,4 +171,22 @@ defmodule Pleroma.Web.AdminAPI.ReportViewTest do
assert report2.id == rendered |> Enum.at(0) |> Map.get(:id)
assert report1.id == rendered |> Enum.at(1) |> Map.get(:id)
end
+
+ test "renders included rules" do
+ user = insert(:user)
+ other_user = insert(:user)
+
+ %{id: rule_id, text: text} = Rule.create(%{text: "Example rule"})
+
+ rule_id = to_string(rule_id)
+
+ {:ok, activity} =
+ CommonAPI.report(user, %{
+ account_id: other_user.id,
+ rule_ids: [rule_id]
+ })
+
+ assert %{rules: [%{id: ^rule_id, text: ^text}]} =
+ ReportView.render("show.json", Report.extract_report_info(activity))
+ end
end
diff --git a/test/pleroma/web/common_api_test.exs b/test/pleroma/web/common_api_test.exs
@@ -12,6 +12,7 @@ defmodule Pleroma.Web.CommonAPITest do
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Repo
+ alias Pleroma.Rule
alias Pleroma.UnstubbedConfigMock, as: ConfigMock
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
@@ -26,8 +27,15 @@ defmodule Pleroma.Web.CommonAPITest do
import Mox
import Pleroma.Factory
+ require Pleroma.Activity.Queries
require Pleroma.Constants
+ defp get_announces_of_object(%{data: %{"id" => id}} = _object) do
+ Pleroma.Activity.Queries.by_type("Announce")
+ |> Pleroma.Activity.Queries.by_object_id(id)
+ |> Pleroma.Repo.all()
+ end
+
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
@@ -499,7 +507,7 @@ defmodule Pleroma.Web.CommonAPITest do
{:ok, convo_reply} =
CommonAPI.post(user, %{status: ".", in_reply_to_conversation_id: participation.id})
- assert Visibility.is_direct?(convo_reply)
+ assert Visibility.direct?(convo_reply)
assert activity.data["context"] == convo_reply.data["context"]
end
@@ -917,7 +925,7 @@ defmodule Pleroma.Web.CommonAPITest do
{:ok, activity} = CommonAPI.post(other_user, %{status: "cofe"})
{:ok, %Activity{} = announce_activity} = CommonAPI.repeat(activity.id, user)
- assert Visibility.is_public?(announce_activity)
+ assert Visibility.public?(announce_activity)
end
test "can't repeat a repeat" do
@@ -939,7 +947,7 @@ defmodule Pleroma.Web.CommonAPITest do
{:ok, %Activity{} = announce_activity} =
CommonAPI.repeat(activity.id, user, %{visibility: "private"})
- assert Visibility.is_private?(announce_activity)
+ assert Visibility.private?(announce_activity)
refute Visibility.visible_for_user?(announce_activity, nil)
end
@@ -952,7 +960,7 @@ defmodule Pleroma.Web.CommonAPITest do
{:ok, %Activity{} = announce_activity} = CommonAPI.repeat(activity.id, author)
- assert Visibility.is_private?(announce_activity)
+ assert Visibility.private?(announce_activity)
refute Visibility.visible_for_user?(announce_activity, nil)
assert Visibility.visible_for_user?(activity, follower)
@@ -1356,6 +1364,33 @@ defmodule Pleroma.Web.CommonAPITest do
assert first_report.data["state"] == "resolved"
assert second_report.data["state"] == "resolved"
end
+
+ test "creates a report with provided rules" do
+ reporter = insert(:user)
+ target_user = insert(:user)
+
+ %{id: rule_id} = Rule.create(%{text: "There are no rules"})
+
+ reporter_ap_id = reporter.ap_id
+ target_ap_id = target_user.ap_id
+
+ report_data = %{
+ account_id: target_user.id,
+ rule_ids: [rule_id]
+ }
+
+ assert {:ok, flag_activity} = CommonAPI.report(reporter, report_data)
+
+ assert %Activity{
+ actor: ^reporter_ap_id,
+ data: %{
+ "type" => "Flag",
+ "object" => [^target_ap_id],
+ "state" => "open",
+ "rules" => [^rule_id]
+ }
+ } = flag_activity
+ end
end
describe "reblog muting" do
@@ -1597,7 +1632,7 @@ defmodule Pleroma.Web.CommonAPITest do
with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do
{:ok, activity} = CommonAPI.post(user, %{status: "#2hu #2HU", visibility: "local"})
- assert Visibility.is_local_public?(activity)
+ assert Visibility.local_public?(activity)
assert_not_called(Pleroma.Web.Federator.publish(activity))
end
end
@@ -1612,7 +1647,7 @@ defmodule Pleroma.Web.CommonAPITest do
assert {:ok, %Activity{data: %{"deleted_activity_id" => ^activity_id}} = activity} =
CommonAPI.delete(activity_id, user)
- assert Visibility.is_local_public?(activity)
+ assert Visibility.local_public?(activity)
assert_not_called(Pleroma.Web.Federator.publish(activity))
end
end
@@ -1628,7 +1663,7 @@ defmodule Pleroma.Web.CommonAPITest do
assert {:ok, %Activity{data: %{"type" => "Announce"}} = activity} =
CommonAPI.repeat(activity_id, user)
- assert Visibility.is_local_public?(activity)
+ assert Visibility.local_public?(activity)
refute called(Pleroma.Web.Federator.publish(activity))
end
end
@@ -1646,7 +1681,7 @@ defmodule Pleroma.Web.CommonAPITest do
assert {:ok, %Activity{data: %{"type" => "Undo"}} = activity} =
CommonAPI.unrepeat(activity_id, user)
- assert Visibility.is_local_public?(activity)
+ assert Visibility.local_public?(activity)
refute called(Pleroma.Web.Federator.publish(activity))
end
end
@@ -1661,7 +1696,7 @@ defmodule Pleroma.Web.CommonAPITest do
assert {:ok, %Activity{data: %{"type" => "Like"}} = activity} =
CommonAPI.favorite(user, activity.id)
- assert Visibility.is_local_public?(activity)
+ assert Visibility.local_public?(activity)
refute called(Pleroma.Web.Federator.publish(activity))
end
end
@@ -1676,7 +1711,7 @@ defmodule Pleroma.Web.CommonAPITest do
with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do
assert {:ok, activity} = CommonAPI.unfavorite(activity.id, user)
- assert Visibility.is_local_public?(activity)
+ assert Visibility.local_public?(activity)
refute called(Pleroma.Web.Federator.publish(activity))
end
end
@@ -1690,7 +1725,7 @@ defmodule Pleroma.Web.CommonAPITest do
assert {:ok, %Activity{data: %{"type" => "EmojiReact"}} = activity} =
CommonAPI.react_with_emoji(activity.id, user, "👍")
- assert Visibility.is_local_public?(activity)
+ assert Visibility.local_public?(activity)
refute called(Pleroma.Web.Federator.publish(activity))
end
end
@@ -1706,7 +1741,7 @@ defmodule Pleroma.Web.CommonAPITest do
assert {:ok, %Activity{data: %{"type" => "Undo"}} = activity} =
CommonAPI.unreact_with_emoji(activity.id, user, "👍")
- assert Visibility.is_local_public?(activity)
+ assert Visibility.local_public?(activity)
refute called(Pleroma.Web.Federator.publish(activity))
end
end
@@ -1835,4 +1870,54 @@ defmodule Pleroma.Web.CommonAPITest do
assert Map.has_key?(updated_object.data, "updated")
end
end
+
+ describe "Group actors" do
+ setup do
+ poster = insert(:user)
+ group = insert(:user, actor_type: "Group")
+ other_group = insert(:user, actor_type: "Group")
+ %{poster: poster, group: group, other_group: other_group}
+ end
+
+ test "it boosts public posts", %{poster: poster, group: group} do
+ {:ok, post} = CommonAPI.post(poster, %{status: "hey @#{group.nickname}"})
+
+ announces = get_announces_of_object(post.object)
+ assert [_] = announces
+ end
+
+ test "it does not boost private posts", %{poster: poster, group: group} do
+ {:ok, private_post} =
+ CommonAPI.post(poster, %{status: "hey @#{group.nickname}", visibility: "private"})
+
+ assert [] = get_announces_of_object(private_post.object)
+ end
+
+ test "remote groups do not boost any posts", %{poster: poster} do
+ remote_group =
+ insert(:user, actor_type: "Group", local: false, nickname: "remote@example.com")
+
+ {:ok, post} = CommonAPI.post(poster, %{status: "hey @#{User.full_nickname(remote_group)}"})
+ assert remote_group.ap_id in post.data["to"]
+
+ announces = get_announces_of_object(post.object)
+ assert [] = announces
+ end
+
+ test "multiple groups mentioned", %{poster: poster, group: group, other_group: other_group} do
+ {:ok, post} =
+ CommonAPI.post(poster, %{status: "hey @#{group.nickname} @#{other_group.nickname}"})
+
+ announces = get_announces_of_object(post.object)
+ assert [_, _] = announces
+ end
+
+ test "it does not boost if group is blocking poster", %{poster: poster, group: group} do
+ {:ok, _} = CommonAPI.block(group, poster)
+ {:ok, post} = CommonAPI.post(poster, %{status: "hey @#{group.nickname}"})
+
+ announces = get_announces_of_object(post.object)
+ assert [] = announces
+ end
+ end
end
diff --git a/test/pleroma/web/federator_test.exs b/test/pleroma/web/federator_test.exs
@@ -40,6 +40,44 @@ defmodule Pleroma.Web.FederatorTest do
%{activity: activity, relay_mock: relay_mock}
end
+ test "to shared inbox when multiple actors from same instance are recipients" do
+ user = insert(:user)
+
+ shared_inbox = "https://domain.com/inbox"
+
+ follower_one =
+ insert(:user, %{
+ local: false,
+ nickname: "nick1@domain.com",
+ ap_id: "https://domain.com/users/nick1",
+ inbox: "https://domain.com/users/nick1/inbox",
+ shared_inbox: shared_inbox
+ })
+
+ follower_two =
+ insert(:user, %{
+ local: false,
+ nickname: "nick2@domain.com",
+ ap_id: "https://domain.com/users/nick2",
+ inbox: "https://domain.com/users/nick2/inbox",
+ shared_inbox: shared_inbox
+ })
+
+ {:ok, _, _} = Pleroma.User.follow(follower_one, user)
+ {:ok, _, _} = Pleroma.User.follow(follower_two, user)
+
+ {:ok, _activity} = CommonAPI.post(user, %{status: "Happy Friday everyone!"})
+
+ ObanHelpers.perform(all_enqueued(worker: PublisherWorker))
+
+ inboxes =
+ all_enqueued(worker: PublisherWorker)
+ |> Enum.filter(&(get_in(&1, [Access.key(:args), Access.key("op")]) == "publish_one"))
+ |> Enum.map(&get_in(&1, [Access.key(:args), Access.key("params"), Access.key("inbox")]))
+
+ assert [shared_inbox] == inboxes
+ end
+
test "with relays active, it publishes to the relay", %{
activity: activity,
relay_mock: relay_mock
diff --git a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs
@@ -1360,7 +1360,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
assert user.registration_reason == "I'm a cool dude, bro"
end
- test "returns error when user already registred", %{conn: conn, valid_params: valid_params} do
+ test "returns error when user already registered", %{conn: conn, valid_params: valid_params} do
_user = insert(:user, email: "lain@example.org")
app_token = insert(:oauth_token, user: nil)
@@ -1495,7 +1495,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
|> Plug.Conn.put_req_header("authorization", "Bearer " <> token)
|> put_req_header("content-type", "multipart/form-data")
|> post("/api/v1/accounts", %{
- nickname: "nickanme",
+ nickname: "nickname",
agreement: true,
email: "email@example.com",
fullname: "Lain",
@@ -1781,7 +1781,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
assert %{language: "ru_RU"} = Pleroma.User.get_by_nickname("foo")
end
- test "createing an account without language parameter should fallback to cookie/header language",
+ test "creating an account without language parameter should fallback to cookie/header language",
%{conn: conn} do
params = %{
username: "foo2",
@@ -2172,6 +2172,55 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
end
end
+ describe "familiar followers" do
+ setup do: oauth_access(["read:follows"])
+
+ test "fetch user familiar followers", %{user: user, conn: conn} do
+ %{id: id1} = other_user1 = insert(:user)
+ %{id: id2} = other_user2 = insert(:user)
+ _ = insert(:user)
+
+ User.follow(user, other_user1)
+ User.follow(other_user1, other_user2)
+
+ assert [%{"accounts" => [%{"id" => ^id1}], "id" => ^id2}] =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> get("/api/v1/accounts/familiar_followers?id[]=#{id2}")
+ |> json_response_and_validate_schema(200)
+ end
+
+ test "returns empty array if followers are hidden", %{user: user, conn: conn} do
+ other_user1 = insert(:user, hide_follows: true)
+ %{id: id2} = other_user2 = insert(:user)
+ _ = insert(:user)
+
+ User.follow(user, other_user1)
+ User.follow(other_user1, other_user2)
+
+ assert [%{"accounts" => [], "id" => ^id2}] =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> get("/api/v1/accounts/familiar_followers?id[]=#{id2}")
+ |> json_response_and_validate_schema(200)
+ end
+
+ test "it respects hide_followers", %{user: user, conn: conn} do
+ other_user1 = insert(:user)
+ %{id: id2} = other_user2 = insert(:user, hide_followers: true)
+ _ = insert(:user)
+
+ User.follow(user, other_user1)
+ User.follow(other_user1, other_user2)
+
+ assert [%{"accounts" => [], "id" => ^id2}] =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> get("/api/v1/accounts/familiar_followers?id[]=#{id2}")
+ |> json_response_and_validate_schema(200)
+ end
+ end
+
describe "remove from followers" do
setup do: oauth_access(["follow"])
diff --git a/test/pleroma/web/mastodon_api/controllers/instance_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/instance_controller_test.exs
@@ -6,6 +6,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceControllerTest do
# TODO: Should not need Cachex
use Pleroma.Web.ConnCase
+ alias Pleroma.Rule
alias Pleroma.User
import Pleroma.Factory
@@ -40,7 +41,8 @@ defmodule Pleroma.Web.MastodonAPI.InstanceControllerTest do
"banner_upload_limit" => _,
"background_image" => from_config_background,
"shout_limit" => _,
- "description_limit" => _
+ "description_limit" => _,
+ "rules" => _
} = result
assert result["pleroma"]["metadata"]["account_activation_required"] != nil
@@ -106,4 +108,48 @@ defmodule Pleroma.Web.MastodonAPI.InstanceControllerTest do
|> get("/api/v1/instance")
|> json_response_and_validate_schema(200)
end
+
+ test "get instance contact information", %{conn: conn} do
+ user = insert(:user, %{local: true})
+
+ clear_config([:instance, :contact_username], user.nickname)
+
+ conn = get(conn, "/api/v1/instance")
+
+ assert result = json_response_and_validate_schema(conn, 200)
+
+ assert result["contact_account"]["id"] == user.id
+ end
+
+ test "get instance information v2", %{conn: conn} do
+ clear_config([:auth, :oauth_consumer_strategies], [])
+
+ assert get(conn, "/api/v2/instance")
+ |> json_response_and_validate_schema(200)
+ end
+
+ test "get instance rules", %{conn: conn} do
+ Rule.create(%{text: "Example rule", hint: "Rule description", priority: 1})
+ Rule.create(%{text: "Third rule", priority: 2})
+ Rule.create(%{text: "Second rule", priority: 1})
+
+ conn = get(conn, "/api/v1/instance")
+
+ assert result = json_response_and_validate_schema(conn, 200)
+
+ assert [
+ %{
+ "text" => "Example rule",
+ "hint" => "Rule description"
+ },
+ %{
+ "text" => "Second rule",
+ "hint" => ""
+ },
+ %{
+ "text" => "Third rule",
+ "hint" => ""
+ }
+ ] = result["rules"]
+ end
end
diff --git a/test/pleroma/web/mastodon_api/controllers/report_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/report_controller_test.exs
@@ -7,6 +7,7 @@ defmodule Pleroma.Web.MastodonAPI.ReportControllerTest do
alias Pleroma.Activity
alias Pleroma.Repo
+ alias Pleroma.Rule
alias Pleroma.Web.CommonAPI
import Pleroma.Factory
@@ -81,6 +82,44 @@ defmodule Pleroma.Web.MastodonAPI.ReportControllerTest do
|> json_response_and_validate_schema(200)
end
+ test "submit a report with rule_ids", %{
+ conn: conn,
+ target_user: target_user
+ } do
+ %{id: rule_id} = Rule.create(%{text: "There are no rules"})
+
+ rule_id = to_string(rule_id)
+
+ assert %{"action_taken" => false, "id" => id} =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> post("/api/v1/reports", %{
+ "account_id" => target_user.id,
+ "forward" => "false",
+ "rule_ids" => [rule_id]
+ })
+ |> json_response_and_validate_schema(200)
+
+ assert %Activity{data: %{"rules" => [^rule_id]}} = Activity.get_report(id)
+ end
+
+ test "rules field is empty if provided wrong rule id", %{
+ conn: conn,
+ target_user: target_user
+ } do
+ assert %{"id" => id} =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> post("/api/v1/reports", %{
+ "account_id" => target_user.id,
+ "forward" => "false",
+ "rule_ids" => ["-1"]
+ })
+ |> json_response_and_validate_schema(200)
+
+ assert %Activity{data: %{"rules" => []}} = Activity.get_report(id)
+ end
+
test "account_id is required", %{
conn: conn,
activity: activity
diff --git a/test/pleroma/web/mastodon_api/controllers/search_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/search_controller_test.exs
@@ -42,6 +42,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do
end
end
+ @tag :skip_darwin
test "search", %{conn: conn} do
user = insert(:user)
user_two = insert(:user, %{nickname: "shp@shitposter.club"})
@@ -321,26 +322,20 @@ defmodule Pleroma.Web.MastodonAPI.SearchControllerTest do
end
test "search fetches remote statuses and prefers them over other results", %{conn: conn} do
- old_version = :persistent_term.get({Pleroma.Repo, :postgres_version})
- :persistent_term.put({Pleroma.Repo, :postgres_version}, 10.0)
- on_exit(fn -> :persistent_term.put({Pleroma.Repo, :postgres_version}, old_version) end)
-
- capture_log(fn ->
- {:ok, %{id: activity_id}} =
- CommonAPI.post(insert(:user), %{
- status: "check out http://mastodon.example.org/@admin/99541947525187367"
- })
+ {:ok, %{id: activity_id}} =
+ CommonAPI.post(insert(:user), %{
+ status: "check out http://mastodon.example.org/@admin/99541947525187367"
+ })
- results =
- conn
- |> get("/api/v1/search?q=http://mastodon.example.org/@admin/99541947525187367")
- |> json_response_and_validate_schema(200)
+ %{"url" => result_url, "id" => result_id} =
+ conn
+ |> get("/api/v1/search?q=http://mastodon.example.org/@admin/99541947525187367")
+ |> json_response_and_validate_schema(200)
+ |> Map.get("statuses")
+ |> List.first()
- assert [
- %{"url" => "http://mastodon.example.org/@admin/99541947525187367"},
- %{"id" => ^activity_id}
- ] = results["statuses"]
- end)
+ refute match?(^result_id, activity_id)
+ assert match?(^result_url, "http://mastodon.example.org/@admin/99541947525187367")
end
test "search doesn't show statuses that it shouldn't", %{conn: conn} do
diff --git a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs
@@ -12,6 +12,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.ScheduledActivity
+ alias Pleroma.Tests.Helpers
alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
@@ -328,68 +329,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
assert real_status == fake_status
end
- test "fake statuses' preview card is not cached", %{conn: conn} do
- Pleroma.StaticStubbedConfigMock
- |> stub(:get, fn
- [:rich_media, :enabled] -> true
- path -> Pleroma.Test.StaticConfig.get(path)
- end)
-
- Tesla.Mock.mock(fn
- %{
- method: :get,
- url: "https://example.com/twitter-card"
- } ->
- %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/twitter_card.html")}
-
- env ->
- apply(HttpRequestMock, :request, [env])
- end)
-
- conn1 =
- conn
- |> put_req_header("content-type", "application/json")
- |> post("/api/v1/statuses", %{
- "status" => "https://example.com/ogp",
- "preview" => true
- })
-
- conn2 =
- conn
- |> put_req_header("content-type", "application/json")
- |> post("/api/v1/statuses", %{
- "status" => "https://example.com/twitter-card",
- "preview" => true
- })
-
- assert %{"card" => %{"title" => "The Rock"}} = json_response_and_validate_schema(conn1, 200)
-
- assert %{"card" => %{"title" => "Small Island Developing States Photo Submission"}} =
- json_response_and_validate_schema(conn2, 200)
- end
-
- test "posting a status with OGP link preview", %{conn: conn} do
- Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
-
- Pleroma.StaticStubbedConfigMock
- |> stub(:get, fn
- [:rich_media, :enabled] -> true
- path -> Pleroma.Test.StaticConfig.get(path)
- end)
-
- conn =
- conn
- |> put_req_header("content-type", "application/json")
- |> post("/api/v1/statuses", %{
- "status" => "https://example.com/ogp"
- })
-
- assert %{"id" => id, "card" => %{"title" => "The Rock"}} =
- json_response_and_validate_schema(conn, 200)
-
- assert Activity.get_by_id(id)
- end
-
test "posting a direct status", %{conn: conn} do
user2 = insert(:user)
content = "direct cofe @#{user2.nickname}"
@@ -1704,91 +1643,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
end
end
- describe "cards" do
- setup do
- Pleroma.StaticStubbedConfigMock
- |> stub(:get, fn
- [:rich_media, :enabled] -> true
- path -> Pleroma.Test.StaticConfig.get(path)
- end)
-
- oauth_access(["read:statuses"])
- end
-
- test "returns rich-media card", %{conn: conn, user: user} do
- Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
-
- {:ok, activity} = CommonAPI.post(user, %{status: "https://example.com/ogp"})
-
- card_data = %{
- "image" => "http://ia.media-imdb.com/images/rock.jpg",
- "provider_name" => "example.com",
- "provider_url" => "https://example.com",
- "title" => "The Rock",
- "type" => "link",
- "url" => "https://example.com/ogp",
- "description" =>
- "Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer.",
- "pleroma" => %{
- "opengraph" => %{
- "image" => "http://ia.media-imdb.com/images/rock.jpg",
- "title" => "The Rock",
- "type" => "video.movie",
- "url" => "https://example.com/ogp",
- "description" =>
- "Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer."
- }
- }
- }
-
- response =
- conn
- |> get("/api/v1/statuses/#{activity.id}/card")
- |> json_response_and_validate_schema(200)
-
- assert response == card_data
-
- # works with private posts
- {:ok, activity} =
- CommonAPI.post(user, %{status: "https://example.com/ogp", visibility: "direct"})
-
- response_two =
- conn
- |> get("/api/v1/statuses/#{activity.id}/card")
- |> json_response_and_validate_schema(200)
-
- assert response_two == card_data
- end
-
- test "replaces missing description with an empty string", %{conn: conn, user: user} do
- Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
-
- {:ok, activity} = CommonAPI.post(user, %{status: "https://example.com/ogp-missing-data"})
-
- response =
- conn
- |> get("/api/v1/statuses/#{activity.id}/card")
- |> json_response_and_validate_schema(:ok)
-
- assert response == %{
- "type" => "link",
- "title" => "Pleroma",
- "description" => "",
- "image" => nil,
- "provider_name" => "example.com",
- "provider_url" => "https://example.com",
- "url" => "https://example.com/ogp-missing-data",
- "pleroma" => %{
- "opengraph" => %{
- "title" => "Pleroma",
- "type" => "website",
- "url" => "https://example.com/ogp-missing-data"
- }
- }
- }
- end
- end
-
test "bookmarks" do
bookmarks_uri = "/api/v1/bookmarks"
@@ -1833,6 +1687,60 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
json_response_and_validate_schema(bookmarks, 200)
end
+ test "bookmark folders" do
+ %{conn: conn, user: user} = oauth_access(["write:bookmarks", "read:bookmarks"])
+
+ {:ok, folder} = Pleroma.BookmarkFolder.create(user.id, "folder")
+ author = insert(:user)
+
+ folder_bookmarks_uri = "/api/v1/bookmarks?folder_id=#{folder.id}"
+
+ {:ok, activity1} = CommonAPI.post(author, %{status: "heweoo?"})
+ {:ok, activity2} = CommonAPI.post(author, %{status: "heweoo!"})
+
+ # Add bookmark with a folder
+ response =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> post("/api/v1/statuses/#{activity1.id}/bookmark", %{folder_id: folder.id})
+
+ assert json_response_and_validate_schema(response, 200)["bookmarked"] == true
+
+ assert json_response_and_validate_schema(response, 200)["pleroma"]["bookmark_folder"] ==
+ folder.id
+
+ response =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> post("/api/v1/statuses/#{activity2.id}/bookmark")
+
+ assert json_response_and_validate_schema(response, 200)["bookmarked"] == true
+ assert json_response_and_validate_schema(response, 200)["pleroma"]["bookmark_folder"] == nil
+
+ bookmarks =
+ get(conn, folder_bookmarks_uri)
+ |> json_response_and_validate_schema(200)
+
+ assert length(bookmarks) == 1
+
+ # Update folder for existing bookmark
+ response =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> post("/api/v1/statuses/#{activity2.id}/bookmark", %{folder_id: folder.id})
+
+ assert json_response_and_validate_schema(response, 200)["bookmarked"] == true
+
+ assert json_response_and_validate_schema(response, 200)["pleroma"]["bookmark_folder"] ==
+ folder.id
+
+ bookmarks =
+ get(conn, folder_bookmarks_uri)
+ |> json_response_and_validate_schema(200)
+
+ assert length(bookmarks) == 2
+ end
+
describe "conversation muting" do
setup do: oauth_access(["write:mutes"])
@@ -2191,7 +2099,10 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
# Using the header for pagination works correctly
[next, _] = get_resp_header(result, "link") |> hd() |> String.split(", ")
- [_, max_id] = Regex.run(~r/max_id=([^&]+)/, next)
+ [next_url, _next_rel] = String.split(next, ";")
+ next_url = String.trim_trailing(next_url, ">") |> String.trim_leading("<")
+
+ max_id = Helpers.get_query_parameter(next_url, "max_id")
assert max_id == third_favorite.id
diff --git a/test/pleroma/web/mastodon_api/controllers/subscription_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/subscription_controller_test.exs
@@ -3,7 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.SubscriptionControllerTest do
- use Pleroma.Web.ConnCase, async: true
+ use Pleroma.Web.ConnCase, async: false
import Pleroma.Factory
@@ -35,17 +35,20 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionControllerTest do
defmacro assert_error_when_disable_push(do: yield) do
quote do
- vapid_details = Application.get_env(:web_push_encryption, :vapid_details, [])
- Application.put_env(:web_push_encryption, :vapid_details, [])
-
assert %{"error" => "Web push subscription is disabled on this Pleroma instance"} ==
unquote(yield)
-
- Application.put_env(:web_push_encryption, :vapid_details, vapid_details)
end
end
describe "when disabled" do
+ setup do
+ vapid_config = Application.get_env(:web_push_encryption, :vapid_details)
+
+ Application.put_env(:web_push_encryption, :vapid_details, [])
+
+ on_exit(fn -> Application.put_env(:web_push_encryption, :vapid_details, vapid_config) end)
+ end
+
test "POST returns error", %{conn: conn} do
assert_error_when_disable_push do
conn
diff --git a/test/pleroma/web/mastodon_api/update_credentials_test.exs b/test/pleroma/web/mastodon_api/update_credentials_test.exs
@@ -511,10 +511,15 @@ defmodule Pleroma.Web.MastodonAPI.UpdateCredentialsTest do
|> json_response_and_validate_schema(200)
assert account_data["fields"] == [
- %{"name" => "<a href=\"http://google.com\">foo</a>", "value" => "bar"},
+ %{
+ "name" => "<a href=\"http://google.com\">foo</a>",
+ "value" => "bar",
+ "verified_at" => nil
+ },
%{
"name" => "link.io",
- "value" => ~S(<a href="http://cofe.io" rel="ugc">cofe.io</a>)
+ "value" => ~S(<a href="http://cofe.io" rel="ugc">cofe.io</a>),
+ "verified_at" => nil
}
]
@@ -573,8 +578,8 @@ defmodule Pleroma.Web.MastodonAPI.UpdateCredentialsTest do
|> json_response_and_validate_schema(200)
assert account_data["fields"] == [
- %{"name" => ":firefox:", "value" => "is best 2hu"},
- %{"name" => "they wins", "value" => ":blank:"}
+ %{"name" => ":firefox:", "value" => "is best 2hu", "verified_at" => nil},
+ %{"name" => "they wins", "value" => ":blank:", "verified_at" => nil}
]
assert account_data["source"]["fields"] == [
@@ -602,10 +607,11 @@ defmodule Pleroma.Web.MastodonAPI.UpdateCredentialsTest do
|> json_response_and_validate_schema(200)
assert account["fields"] == [
- %{"name" => "foo", "value" => "bar"},
+ %{"name" => "foo", "value" => "bar", "verified_at" => nil},
%{
"name" => "link",
- "value" => ~S(<a href="http://cofe.io" rel="ugc">http://cofe.io</a>)
+ "value" => ~S(<a href="http://cofe.io" rel="ugc">http://cofe.io</a>),
+ "verified_at" => nil
}
]
@@ -627,7 +633,7 @@ defmodule Pleroma.Web.MastodonAPI.UpdateCredentialsTest do
|> json_response_and_validate_schema(200)
assert account["fields"] == [
- %{"name" => "foo", "value" => ""}
+ %{"name" => "foo", "value" => "", "verified_at" => nil}
]
end
@@ -732,4 +738,20 @@ defmodule Pleroma.Web.MastodonAPI.UpdateCredentialsTest do
assert account["source"]["pleroma"]["actor_type"] == "Person"
end
end
+
+ describe "Mark account as group" do
+ setup do: oauth_access(["write:accounts"])
+ setup :request_content_type
+
+ test "changing actor_type to Group makes account a Group and enables bot indicator for backward compatibility",
+ %{conn: conn} do
+ account =
+ conn
+ |> patch("/api/v1/accounts/update_credentials", %{actor_type: "Group"})
+ |> json_response_and_validate_schema(200)
+
+ assert account["bot"]
+ assert account["source"]["pleroma"]["actor_type"] == "Group"
+ end
+ end
end
diff --git a/test/pleroma/web/mastodon_api/views/poll_view_test.exs b/test/pleroma/web/mastodon_api/views/poll_view_test.exs
@@ -43,7 +43,8 @@ defmodule Pleroma.Web.MastodonAPI.PollViewTest do
%{title: "why are you even asking?", votes_count: 0}
],
votes_count: 0,
- voters_count: 0
+ voters_count: 0,
+ pleroma: %{non_anonymous: false}
}
result = PollView.render("show.json", %{object: object})
@@ -165,4 +166,11 @@ defmodule Pleroma.Web.MastodonAPI.PollViewTest do
]
} = PollView.render("show.json", %{object: object})
end
+
+ test "that poll is non anonymous" do
+ object = Object.normalize("https://friends.grishka.me/posts/54642", fetch: true)
+ result = PollView.render("show.json", %{object: object})
+
+ assert result[:pleroma][:non_anonymous] == true
+ end
end
diff --git a/test/pleroma/web/mastodon_api/views/status_view_test.exs b/test/pleroma/web/mastodon_api/views/status_view_test.exs
@@ -17,6 +17,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.StatusView
+ alias Pleroma.Web.RichMedia.Card
require Bitwise
@@ -341,7 +342,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
emoji_reactions: [],
parent_visible: false,
pinned_at: nil,
- quotes_count: 0
+ quotes_count: 0,
+ bookmark_folder: nil
}
}
@@ -731,56 +733,72 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
describe "rich media cards" do
test "a rich media card without a site name renders correctly" do
- page_url = "http://example.com"
+ page_url = "https://example.com"
- card = %{
- url: page_url,
- image: page_url <> "/example.jpg",
- title: "Example website"
- }
+ {:ok, card} =
+ Card.create(page_url, %{image: page_url <> "/example.jpg", title: "Example website"})
- %{provider_name: "example.com"} =
- StatusView.render("card.json", %{page_url: page_url, rich_media: card})
+ assert match?(%{provider_name: "example.com"}, StatusView.render("card.json", card))
end
test "a rich media card without a site name or image renders correctly" do
- page_url = "http://example.com"
+ page_url = "https://example.com"
- card = %{
- url: page_url,
- title: "Example website"
+ fields = %{
+ "url" => page_url,
+ "title" => "Example website"
}
- %{provider_name: "example.com"} =
- StatusView.render("card.json", %{page_url: page_url, rich_media: card})
+ {:ok, card} = Card.create(page_url, fields)
+
+ assert match?(%{provider_name: "example.com"}, StatusView.render("card.json", card))
end
test "a rich media card without an image renders correctly" do
- page_url = "http://example.com"
+ page_url = "https://example.com"
+
+ fields = %{
+ "url" => page_url,
+ "site_name" => "Example site name",
+ "title" => "Example website"
+ }
+
+ {:ok, card} = Card.create(page_url, fields)
- card = %{
- url: page_url,
- site_name: "Example site name",
- title: "Example website"
+ assert match?(%{provider_name: "example.com"}, StatusView.render("card.json", card))
+ end
+
+ test "a rich media card without descriptions returns the fields with empty strings" do
+ page_url = "https://example.com"
+
+ fields = %{
+ "url" => page_url,
+ "site_name" => "Example site name",
+ "title" => "Example website"
}
- %{provider_name: "example.com"} =
- StatusView.render("card.json", %{page_url: page_url, rich_media: card})
+ {:ok, card} = Card.create(page_url, fields)
+
+ assert match?(
+ %{description: "", image_description: ""},
+ StatusView.render("card.json", card)
+ )
end
test "a rich media card with all relevant data renders correctly" do
- page_url = "http://example.com"
-
- card = %{
- url: page_url,
- site_name: "Example site name",
- title: "Example website",
- image: page_url <> "/example.jpg",
- description: "Example description"
+ page_url = "https://example.com"
+
+ fields = %{
+ "url" => page_url,
+ "site_name" => "Example site name",
+ "title" => "Example website",
+ "image" => page_url <> "/example.jpg",
+ "description" => "Example description"
}
- %{provider_name: "example.com"} =
- StatusView.render("card.json", %{page_url: page_url, rich_media: card})
+ {:ok, card} = Card.create(page_url, fields)
+
+ assert match?(%{provider_name: "example.com"}, StatusView.render("card.json", card))
end
test "a rich media card has all media proxied" do
@@ -790,25 +808,25 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
ConfigMock
|> stub_with(Pleroma.Test.StaticConfig)
- page_url = "http://example.com"
+ page_url = "https://example.com"
- card = %{
- url: page_url,
- site_name: "Example site name",
- title: "Example website",
- image: page_url <> "/example.jpg",
- audio: page_url <> "/example.ogg",
- video: page_url <> "/example.mp4",
- description: "Example description"
+ fields = %{
+ "url" => page_url,
+ "site_name" => "Example site name",
+ "title" => "Example website",
+ "image" => page_url <> "/example.jpg",
+ "audio" => page_url <> "/example.ogg",
+ "video" => page_url <> "/example.mp4",
+ "description" => "Example description"
}
- strcard = for {k, v} <- card, into: %{}, do: {to_string(k), v}
+ {:ok, card} = Card.create(page_url, fields)
%{
provider_name: "example.com",
image: image,
pleroma: %{opengraph: og}
- } = StatusView.render("card.json", %{page_url: page_url, rich_media: strcard})
+ } = StatusView.render("card.json", card)
assert String.match?(image, ~r/\/proxy\//)
assert String.match?(og["image"], ~r/\/proxy\//)
diff --git a/test/pleroma/web/media_proxy/media_proxy_controller_test.exs b/test/pleroma/web/media_proxy/media_proxy_controller_test.exs
@@ -182,7 +182,7 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do
media_proxy_url: media_proxy_url
} do
Tesla.Mock.mock(fn
- %{method: "HEAD", url: ^media_proxy_url} ->
+ %{method: :head, url: ^media_proxy_url} ->
%Tesla.Env{status: 500, body: ""}
end)
@@ -197,7 +197,7 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do
media_proxy_url: media_proxy_url
} do
Tesla.Mock.mock(fn
- %{method: "HEAD", url: ^media_proxy_url} ->
+ %{method: :head, url: ^media_proxy_url} ->
%Tesla.Env{status: 200, body: "", headers: [{"content-type", "application/pdf"}]}
end)
@@ -217,7 +217,7 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do
clear_config([:media_preview_proxy, :min_content_length], 1_000_000_000)
Tesla.Mock.mock(fn
- %{method: "HEAD", url: ^media_proxy_url} ->
+ %{method: :head, url: ^media_proxy_url} ->
%Tesla.Env{
status: 200,
body: "",
@@ -242,7 +242,7 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do
media_proxy_url: media_proxy_url
} do
Tesla.Mock.mock(fn
- %{method: "HEAD", url: ^media_proxy_url} ->
+ %{method: :head, url: ^media_proxy_url} ->
%Tesla.Env{status: 200, body: "", headers: [{"content-type", "image/gif"}]}
end)
@@ -260,7 +260,7 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do
media_proxy_url: media_proxy_url
} do
Tesla.Mock.mock(fn
- %{method: "HEAD", url: ^media_proxy_url} ->
+ %{method: :head, url: ^media_proxy_url} ->
%Tesla.Env{status: 200, body: "", headers: [{"content-type", "image/jpeg"}]}
end)
@@ -280,7 +280,7 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do
clear_config([:media_preview_proxy, :min_content_length], 100_000)
Tesla.Mock.mock(fn
- %{method: "HEAD", url: ^media_proxy_url} ->
+ %{method: :head, url: ^media_proxy_url} ->
%Tesla.Env{
status: 200,
body: "",
@@ -302,7 +302,7 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do
assert_dependencies_installed()
Tesla.Mock.mock(fn
- %{method: "HEAD", url: ^media_proxy_url} ->
+ %{method: :head, url: ^media_proxy_url} ->
%Tesla.Env{status: 200, body: "", headers: [{"content-type", "image/png"}]}
%{method: :get, url: ^media_proxy_url} ->
@@ -324,7 +324,7 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do
assert_dependencies_installed()
Tesla.Mock.mock(fn
- %{method: "HEAD", url: ^media_proxy_url} ->
+ %{method: :head, url: ^media_proxy_url} ->
%Tesla.Env{status: 200, body: "", headers: [{"content-type", "image/jpeg"}]}
%{method: :get, url: ^media_proxy_url} ->
@@ -344,7 +344,7 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do
media_proxy_url: media_proxy_url
} do
Tesla.Mock.mock(fn
- %{method: "HEAD", url: ^media_proxy_url} ->
+ %{method: :head, url: ^media_proxy_url} ->
%Tesla.Env{status: 200, body: "", headers: [{"content-type", "image/jpeg"}]}
%{method: :get, url: ^media_proxy_url} ->
diff --git a/test/pleroma/web/o_auth/mfa_controller_test.exs b/test/pleroma/web/o_auth/mfa_controller_test.exs
@@ -214,7 +214,7 @@ defmodule Pleroma.Web.OAuth.MFAControllerTest do
assert response == %{"error" => "Invalid code"}
end
- test "returns error when client credentails is wrong ", %{conn: conn, user: user} do
+ test "returns error when client credentials is wrong ", %{conn: conn, user: user} do
otp_token = TOTP.generate_token(user.multi_factor_authentication_settings.totp.secret)
mfa_token = insert(:mfa_token, user: user)
diff --git a/test/pleroma/web/o_auth/token/utils_test.exs b/test/pleroma/web/o_auth/token/utils_test.exs
@@ -13,7 +13,7 @@ defmodule Pleroma.Web.OAuth.Token.UtilsTest do
Utils.fetch_app(%Plug.Conn{params: %{"client_id" => 1, "client_secret" => "x"}})
end
- test "returns App by params credentails" do
+ test "returns App by params credentials" do
app = insert(:oauth_app)
assert {:ok, load_app} =
@@ -24,7 +24,7 @@ defmodule Pleroma.Web.OAuth.Token.UtilsTest do
assert load_app == app
end
- test "returns App by header credentails" do
+ test "returns App by header credentials" do
app = insert(:oauth_app)
header = "Basic " <> Base.encode64("#{app.client_id}:#{app.client_secret}")
diff --git a/test/pleroma/web/pleroma_api/controllers/bookmark_folder_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/bookmark_folder_controller_test.exs
@@ -0,0 +1,161 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+defmodule Pleroma.Web.PleromaAPI.BookmarkFolderControllerTest do
+ use Pleroma.Web.ConnCase
+
+ alias Pleroma.BookmarkFolder
+ # alias Pleroma.Object
+ # alias Pleroma.Tests.Helpers
+ # alias Pleroma.UnstubbedConfigMock, as: ConfigMock
+ # alias Pleroma.User
+ # alias Pleroma.Web.ActivityPub.ActivityPub
+ # alias Pleroma.Web.CommonAPI
+
+ # import Mox
+ import Pleroma.Factory
+
+ describe "GET /api/v1/pleroma/bookmark_folders" do
+ setup do: oauth_access(["read:bookmarks"])
+
+ test "it lists bookmark folders", %{conn: conn, user: user} do
+ {:ok, folder} = BookmarkFolder.create(user.id, "Bookmark folder")
+
+ folder_id = folder.id
+
+ result =
+ conn
+ |> get("/api/v1/pleroma/bookmark_folders")
+ |> json_response_and_validate_schema(200)
+
+ assert [
+ %{
+ "id" => ^folder_id,
+ "name" => "Bookmark folder",
+ "emoji" => nil,
+ "emoji_url" => nil
+ }
+ ] = result
+ end
+ end
+
+ describe "POST /api/v1/pleroma/bookmark_folders" do
+ setup do: oauth_access(["write:bookmarks"])
+
+ test "it creates a bookmark folder", %{conn: conn} do
+ result =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> post("/api/v1/pleroma/bookmark_folders", %{
+ name: "Bookmark folder",
+ emoji: "📁"
+ })
+ |> json_response_and_validate_schema(200)
+
+ assert %{
+ "name" => "Bookmark folder",
+ "emoji" => "📁",
+ "emoji_url" => nil
+ } = result
+ end
+
+ test "it creates a bookmark folder with custom emoji", %{conn: conn} do
+ result =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> post("/api/v1/pleroma/bookmark_folders", %{
+ name: "Bookmark folder",
+ emoji: ":firefox:"
+ })
+ |> json_response_and_validate_schema(200)
+
+ assert %{
+ "name" => "Bookmark folder",
+ "emoji" => ":firefox:",
+ "emoji_url" => "http://localhost:4001/emoji/Firefox.gif"
+ } = result
+ end
+
+ test "it returns error for invalid emoji", %{conn: conn} do
+ result =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> post("/api/v1/pleroma/bookmark_folders", %{
+ name: "Bookmark folder",
+ emoji: "not an emoji"
+ })
+ |> json_response_and_validate_schema(422)
+
+ assert %{"error" => "Invalid emoji"} = result
+ end
+ end
+
+ describe "PATCH /api/v1/pleroma/bookmark_folders/:id" do
+ setup do: oauth_access(["write:bookmarks"])
+
+ test "it updates a bookmark folder", %{conn: conn, user: user} do
+ {:ok, folder} = BookmarkFolder.create(user.id, "Bookmark folder")
+
+ result =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> patch("/api/v1/pleroma/bookmark_folders/#{folder.id}", %{
+ name: "bookmark folder"
+ })
+ |> json_response_and_validate_schema(200)
+
+ assert %{
+ "name" => "bookmark folder"
+ } = result
+ end
+
+ test "it returns error when updating others' folders", %{conn: conn} do
+ other_user = insert(:user)
+
+ {:ok, folder} = BookmarkFolder.create(other_user.id, "Bookmark folder")
+
+ result =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> patch("/api/v1/pleroma/bookmark_folders/#{folder.id}", %{
+ name: "bookmark folder"
+ })
+ |> json_response_and_validate_schema(403)
+
+ assert %{
+ "error" => "Access denied"
+ } = result
+ end
+ end
+
+ describe "DELETE /api/v1/pleroma/bookmark_folders/:id" do
+ setup do: oauth_access(["write:bookmarks"])
+
+ test "it deleting a bookmark folder", %{conn: conn, user: user} do
+ {:ok, folder} = BookmarkFolder.create(user.id, "Bookmark folder")
+
+ assert conn
+ |> delete("/api/v1/pleroma/bookmark_folders/#{folder.id}")
+ |> json_response_and_validate_schema(200)
+
+ folders = BookmarkFolder.for_user(user.id)
+
+ assert length(folders) == 0
+ end
+
+ test "it returns error when deleting others' folders", %{conn: conn} do
+ other_user = insert(:user)
+
+ {:ok, folder} = BookmarkFolder.create(other_user.id, "Bookmark folder")
+
+ result =
+ conn
+ |> patch("/api/v1/pleroma/bookmark_folders/#{folder.id}")
+ |> json_response_and_validate_schema(403)
+
+ assert %{
+ "error" => "Access denied"
+ } = result
+ end
+ end
+end
diff --git a/test/pleroma/web/pleroma_api/controllers/chat_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/chat_controller_test.exs
@@ -7,6 +7,7 @@ defmodule Pleroma.Web.PleromaAPI.ChatControllerTest do
alias Pleroma.Chat
alias Pleroma.Chat.MessageReference
alias Pleroma.Object
+ alias Pleroma.Tests.Helpers
alias Pleroma.UnstubbedConfigMock, as: ConfigMock
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
@@ -212,36 +213,63 @@ defmodule Pleroma.Web.PleromaAPI.ChatControllerTest do
result = json_response_and_validate_schema(response, 200)
[next, prev] = get_resp_header(response, "link") |> hd() |> String.split(", ")
- api_endpoint = "/api/v1/pleroma/chats/"
+ api_endpoint = Pleroma.Web.Endpoint.url() <> "/api/v1/pleroma/chats/"
+
+ [next_url, next_rel] = String.split(next, ";")
+ next_url = String.trim_trailing(next_url, ">") |> String.trim_leading("<")
+
+ next_url_sorted = Helpers.uri_query_sort(next_url)
assert String.match?(
- next,
- ~r(#{api_endpoint}.*/messages\?limit=\d+&max_id=.*; rel=\"next\"$)
+ next_url_sorted,
+ ~r(#{api_endpoint}.*/messages\?limit=\d+&max_id=.*&offset=\d+$)
)
+ assert next_rel =~ "next"
+
+ [prev_url, prev_rel] = String.split(prev, ";")
+ prev_url = String.trim_trailing(prev_url, ">") |> String.trim_leading("<")
+
+ prev_url_sorted = Helpers.uri_query_sort(prev_url)
+
assert String.match?(
- prev,
- ~r(#{api_endpoint}.*/messages\?limit=\d+&min_id=.*; rel=\"prev\"$)
+ prev_url_sorted,
+ ~r(#{api_endpoint}.*/messages\?limit=\d+&min_id=.*&offset=\d+$)
)
+ assert prev_rel =~ "prev"
+
assert length(result) == 20
- response =
- get(conn, "/api/v1/pleroma/chats/#{chat.id}/messages?max_id=#{List.last(result)["id"]}")
+ response = get(conn, "#{api_endpoint}#{chat.id}/messages?max_id=#{List.last(result)["id"]}")
result = json_response_and_validate_schema(response, 200)
[next, prev] = get_resp_header(response, "link") |> hd() |> String.split(", ")
+ [next_url, next_rel] = String.split(next, ";")
+ next_url = String.trim_trailing(next_url, ">") |> String.trim_leading("<")
+
+ next_url_sorted = Helpers.uri_query_sort(next_url)
+
assert String.match?(
- next,
- ~r(#{api_endpoint}.*/messages\?limit=\d+&max_id=.*; rel=\"next\"$)
+ next_url_sorted,
+ ~r(#{api_endpoint}.*/messages\?limit=\d+&max_id=.*&offset=\d+$)
)
+ assert next_rel =~ "next"
+
+ [prev_url, prev_rel] = String.split(prev, ";")
+ prev_url = String.trim_trailing(prev_url, ">") |> String.trim_leading("<")
+
+ prev_url_sorted = Helpers.uri_query_sort(prev_url)
+
assert String.match?(
- prev,
- ~r(#{api_endpoint}.*/messages\?limit=\d+&max_id=.*&min_id=.*; rel=\"prev\"$)
+ prev_url_sorted,
+ ~r(#{api_endpoint}.*/messages\?limit=\d+&max_id=.*&min_id=.*&offset=\d+$)
)
+ assert prev_rel =~ "prev"
+
assert length(result) == 10
end
diff --git a/test/pleroma/web/pleroma_api/controllers/emoji_pack_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/emoji_pack_controller_test.exs
@@ -116,7 +116,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackControllerTest do
%{method: :get, url: "https://example.com/nodeinfo/2.1.json"} ->
json(%{metadata: %{features: ["shareable_emoji_packs"]}})
- %{method: :get, url: "https://example.com/api/pleroma/emoji/packs?page=2&page_size=1"} ->
+ %{method: :get, url: "https://example.com/api/v1/pleroma/emoji/packs?page=2&page_size=1"} ->
json(resp)
end)
@@ -199,7 +199,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackControllerTest do
%{
method: :get,
- url: "https://example.com/api/pleroma/emoji/pack?name=test_pack"
+ url: "https://example.com/api/v1/pleroma/emoji/pack?name=test_pack&page_size=" <> _n
} ->
conn
|> get("/api/pleroma/emoji/pack?name=test_pack")
@@ -208,7 +208,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackControllerTest do
%{
method: :get,
- url: "https://example.com/api/pleroma/emoji/packs/archive?name=test_pack"
+ url: "https://example.com/api/v1/pleroma/emoji/packs/archive?name=test_pack"
} ->
conn
|> get("/api/pleroma/emoji/packs/archive?name=test_pack")
@@ -217,7 +217,9 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackControllerTest do
%{
method: :get,
- url: "https://example.com/api/pleroma/emoji/pack?name=test_pack_nonshared"
+ url:
+ "https://example.com/api/v1/pleroma/emoji/pack?name=test_pack_nonshared&page_size=" <>
+ _n
} ->
conn
|> get("/api/pleroma/emoji/pack?name=test_pack_nonshared")
@@ -305,14 +307,14 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackControllerTest do
%{
method: :get,
- url: "https://example.com/api/pleroma/emoji/pack?name=pack_bad_sha"
+ url: "https://example.com/api/v1/pleroma/emoji/pack?name=pack_bad_sha&page_size=" <> _n
} ->
{:ok, pack} = Pleroma.Emoji.Pack.load_pack("pack_bad_sha")
%Tesla.Env{status: 200, body: Jason.encode!(pack)}
%{
method: :get,
- url: "https://example.com/api/pleroma/emoji/packs/archive?name=pack_bad_sha"
+ url: "https://example.com/api/v1/pleroma/emoji/packs/archive?name=pack_bad_sha"
} ->
%Tesla.Env{
status: 200,
@@ -342,7 +344,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiPackControllerTest do
%{
method: :get,
- url: "https://example.com/api/pleroma/emoji/pack?name=test_pack"
+ url: "https://example.com/api/v1/pleroma/emoji/pack?name=test_pack&page_size=" <> _n
} ->
{:ok, pack} = Pleroma.Emoji.Pack.load_pack("test_pack")
%Tesla.Env{status: 200, body: Jason.encode!(pack)}
diff --git a/test/pleroma/web/pleroma_api/controllers/instances_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/instances_controller_test.exs
@@ -16,7 +16,7 @@ defmodule Pleroma.Web.PleromaApi.InstancesControllerTest do
{:ok, %Pleroma.Instances.Instance{unreachable_since: constant_unreachable}} =
Instances.set_consistently_unreachable(constant)
- _eventual_unrechable = Instances.set_unreachable(eventual)
+ _eventual_unreachable = Instances.set_unreachable(eventual)
%{constant_unreachable: constant_unreachable, constant: constant}
end
diff --git a/test/pleroma/web/pleroma_api/controllers/notification_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/notification_controller_test.exs
@@ -21,13 +21,11 @@ defmodule Pleroma.Web.PleromaAPI.NotificationControllerTest do
{:ok, [notification1]} = Notification.create_notifications(activity1)
{:ok, [notification2]} = Notification.create_notifications(activity2)
- response =
- conn
- |> put_req_header("content-type", "application/json")
- |> post("/api/v1/pleroma/notifications/read", %{id: notification1.id})
- |> json_response_and_validate_schema(:ok)
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> post("/api/v1/pleroma/notifications/read", %{id: notification1.id})
+ |> json_response_and_validate_schema(:ok)
- assert %{"pleroma" => %{"is_seen" => true}} = response
assert Repo.get(Notification, notification1.id).seen
refute Repo.get(Notification, notification2.id).seen
end
@@ -40,14 +38,17 @@ defmodule Pleroma.Web.PleromaAPI.NotificationControllerTest do
[notification3, notification2, notification1] = Notification.for_user(user1, %{limit: 3})
- [response1, response2] =
- conn
- |> put_req_header("content-type", "application/json")
- |> post("/api/v1/pleroma/notifications/read", %{max_id: notification2.id})
- |> json_response_and_validate_schema(:ok)
+ refute Repo.get(Notification, notification1.id).seen
+ refute Repo.get(Notification, notification2.id).seen
+ refute Repo.get(Notification, notification3.id).seen
+
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> post("/api/v1/pleroma/notifications/read", %{max_id: notification2.id})
+ |> json_response_and_validate_schema(:ok)
+
+ [notification3, notification2, notification1] = Notification.for_user(user1, %{limit: 3})
- assert %{"pleroma" => %{"is_seen" => true}} = response1
- assert %{"pleroma" => %{"is_seen" => true}} = response2
assert Repo.get(Notification, notification1.id).seen
assert Repo.get(Notification, notification2.id).seen
refute Repo.get(Notification, notification3.id).seen
diff --git a/test/pleroma/web/pleroma_api/views/chat_message_reference_view_test.exs b/test/pleroma/web/pleroma_api/views/chat_message_reference_view_test.exs
@@ -9,7 +9,6 @@ defmodule Pleroma.Web.PleromaAPI.ChatMessageReferenceViewTest do
alias Pleroma.Chat
alias Pleroma.Chat.MessageReference
alias Pleroma.Object
- alias Pleroma.StaticStubbedConfigMock
alias Pleroma.UnstubbedConfigMock, as: ConfigMock
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.CommonAPI
@@ -18,6 +17,8 @@ defmodule Pleroma.Web.PleromaAPI.ChatMessageReferenceViewTest do
import Mox
import Pleroma.Factory
+ setup do: clear_config([:rich_media, :enabled], true)
+
test "it displays a chat message" do
user = insert(:user)
recipient = insert(:user)
@@ -49,6 +50,7 @@ defmodule Pleroma.Web.PleromaAPI.ChatMessageReferenceViewTest do
:chat_message_id_idempotency_key_cache, ^id -> {:ok, "123"}
cache, key -> NullCache.get(cache, key)
end)
+ |> stub(:fetch, fn :rich_media_cache, _, _ -> {:ok, {:ok, %{}}} end)
chat_message = MessageReferenceView.render("show.json", chat_message_reference: cm_ref)
@@ -61,16 +63,7 @@ defmodule Pleroma.Web.PleromaAPI.ChatMessageReferenceViewTest do
assert match?([%{shortcode: "firefox"}], chat_message[:emojis])
assert chat_message[:idempotency_key] == "123"
- StaticStubbedConfigMock
- |> stub(:get, fn
- [:rich_media, :enabled] -> true
- path -> Pleroma.Test.StaticConfig.get(path)
- end)
-
- Tesla.Mock.mock_global(fn
- %{url: "https://example.com/ogp"} ->
- %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/ogp.html")}
- end)
+ Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
{:ok, activity} =
CommonAPI.post_chat_message(recipient, user, "gkgkgk https://example.com/ogp",
diff --git a/test/pleroma/web/rich_media/card_test.exs b/test/pleroma/web/rich_media/card_test.exs
@@ -0,0 +1,71 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.RichMedia.CardTest do
+ use Pleroma.DataCase, async: true
+
+ alias Pleroma.UnstubbedConfigMock, as: ConfigMock
+ alias Pleroma.Web.CommonAPI
+ alias Pleroma.Web.RichMedia.Card
+
+ import Mox
+ import Pleroma.Factory
+ import Tesla.Mock
+
+ setup do
+ mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
+
+ ConfigMock
+ |> stub_with(Pleroma.Test.StaticConfig)
+
+ :ok
+ end
+
+ setup do: clear_config([:rich_media, :enabled], true)
+
+ test "crawls URL in activity" do
+ user = insert(:user)
+
+ url = "https://example.com/ogp"
+ url_hash = Card.url_to_hash(url)
+
+ {:ok, activity} =
+ CommonAPI.post(user, %{
+ status: "[test](#{url})",
+ content_type: "text/markdown"
+ })
+
+ assert %Card{url_hash: ^url_hash, fields: _} = Card.get_by_activity(activity)
+ end
+
+ test "recrawls URLs on status edits/updates" do
+ original_url = "https://google.com/"
+ original_url_hash = Card.url_to_hash(original_url)
+ updated_url = "https://yahoo.com/"
+ updated_url_hash = Card.url_to_hash(updated_url)
+
+ user = insert(:user)
+ {:ok, activity} = CommonAPI.post(user, %{status: "I like this site #{original_url}"})
+
+ # Force a backfill
+ Card.get_by_activity(activity)
+
+ assert match?(
+ %Card{url_hash: ^original_url_hash, fields: _},
+ Card.get_by_activity(activity)
+ )
+
+ {:ok, _} = CommonAPI.update(user, activity, %{status: "I like this site #{updated_url}"})
+
+ activity = Pleroma.Activity.get_by_id(activity.id)
+
+ # Force a backfill
+ Card.get_by_activity(activity)
+
+ assert match?(
+ %Card{url_hash: ^updated_url_hash, fields: _},
+ Card.get_by_activity(activity)
+ )
+ end
+end
diff --git a/test/pleroma/web/rich_media/helpers_test.exs b/test/pleroma/web/rich_media/helpers_test.exs
@@ -1,111 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.RichMedia.HelpersTest do
- use Pleroma.DataCase, async: true
-
- alias Pleroma.StaticStubbedConfigMock, as: ConfigMock
- alias Pleroma.Web.CommonAPI
- alias Pleroma.Web.RichMedia.Helpers
-
- import Mox
- import Pleroma.Factory
- import Tesla.Mock
-
- setup do
- mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
-
- ConfigMock
- |> stub(:get, fn
- [:rich_media, :enabled] -> false
- path -> Pleroma.Test.StaticConfig.get(path)
- end)
- |> stub(:get, fn
- path, default -> Pleroma.Test.StaticConfig.get(path, default)
- end)
-
- :ok
- end
-
- test "refuses to crawl incomplete URLs" do
- user = insert(:user)
-
- {:ok, activity} =
- CommonAPI.post(user, %{
- status: "[test](example.com/ogp)",
- content_type: "text/markdown"
- })
-
- ConfigMock
- |> stub(:get, fn
- [:rich_media, :enabled] -> true
- path -> Pleroma.Test.StaticConfig.get(path)
- end)
-
- assert %{} == Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
- end
-
- test "refuses to crawl malformed URLs" do
- user = insert(:user)
-
- {:ok, activity} =
- CommonAPI.post(user, %{
- status: "[test](example.com[]/ogp)",
- content_type: "text/markdown"
- })
-
- ConfigMock
- |> stub(:get, fn
- [:rich_media, :enabled] -> true
- path -> Pleroma.Test.StaticConfig.get(path)
- end)
-
- assert %{} == Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
- end
-
- test "crawls valid, complete URLs" do
- user = insert(:user)
-
- {:ok, activity} =
- CommonAPI.post(user, %{
- status: "[test](https://example.com/ogp)",
- content_type: "text/markdown"
- })
-
- ConfigMock
- |> stub(:get, fn
- [:rich_media, :enabled] -> true
- path -> Pleroma.Test.StaticConfig.get(path)
- end)
-
- assert %{page_url: "https://example.com/ogp", rich_media: _} =
- Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
- end
-
- # This does not seem to work. The urls are being fetched.
- @tag skip: true
- test "refuses to crawl URLs of private network from posts" do
- user = insert(:user)
-
- {:ok, activity} =
- CommonAPI.post(user, %{status: "http://127.0.0.1:4000/notice/9kCP7VNyPJXFOXDrgO"})
-
- {:ok, activity2} = CommonAPI.post(user, %{status: "https://10.111.10.1/notice/9kCP7V"})
- {:ok, activity3} = CommonAPI.post(user, %{status: "https://172.16.32.40/notice/9kCP7V"})
- {:ok, activity4} = CommonAPI.post(user, %{status: "https://192.168.10.40/notice/9kCP7V"})
- {:ok, activity5} = CommonAPI.post(user, %{status: "https://pleroma.local/notice/9kCP7V"})
-
- ConfigMock
- |> stub(:get, fn
- [:rich_media, :enabled] -> true
- path -> Pleroma.Test.StaticConfig.get(path)
- end)
-
- assert %{} = Helpers.fetch_data_for_activity(activity)
- assert %{} = Helpers.fetch_data_for_activity(activity2)
- assert %{} = Helpers.fetch_data_for_activity(activity3)
- assert %{} = Helpers.fetch_data_for_activity(activity4)
- assert %{} = Helpers.fetch_data_for_activity(activity5)
- end
-end
diff --git a/test/pleroma/web/rich_media/parser/ttl/aws_signed_url_test.exs b/test/pleroma/web/rich_media/parser/ttl/aws_signed_url_test.exs
@@ -3,8 +3,22 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrlTest do
- # Relies on Cachex, needs to be synchronous
- use Pleroma.DataCase
+ use Pleroma.DataCase, async: false
+ use Oban.Testing, repo: Pleroma.Repo
+
+ import Mox
+
+ alias Pleroma.UnstubbedConfigMock, as: ConfigMock
+ alias Pleroma.Web.RichMedia.Card
+
+ setup do
+ ConfigMock
+ |> stub_with(Pleroma.Test.StaticConfig)
+
+ clear_config([:rich_media, :enabled], true)
+
+ :ok
+ end
test "s3 signed url is parsed correct for expiration time" do
url = "https://pleroma.social/amz"
@@ -22,7 +36,7 @@ defmodule Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrlTest do
expire_time =
Timex.parse!(timestamp, "{ISO:Basic:Z}") |> Timex.to_unix() |> Kernel.+(valid_till)
- assert {:ok, expire_time} == Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl.ttl(metadata, url)
+ assert expire_time == Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl.ttl(metadata, url)
end
test "s3 signed url is parsed and correct ttl is set for rich media" do
@@ -43,26 +57,29 @@ defmodule Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrlTest do
<meta name="twitter:site" content="Pleroma" />
<meta name="twitter:title" content="Pleroma" />
<meta name="twitter:description" content="Pleroma" />
- <meta name="twitter:image" content="#{Map.get(metadata, :image)}" />
+ <meta name="twitter:image" content="#{Map.get(metadata, "image")}" />
"""
Tesla.Mock.mock(fn
%{
method: :get,
- url: "https://pleroma.social/amz"
+ url: ^url
} ->
%Tesla.Env{status: 200, body: body}
+
+ %{method: :head} ->
+ %Tesla.Env{status: 200}
end)
- Cachex.put(:rich_media_cache, url, metadata)
+ Card.get_or_backfill_by_url(url)
+
+ assert_enqueued(worker: Pleroma.Workers.RichMediaExpirationWorker, args: %{"url" => url})
- Pleroma.Web.RichMedia.Parser.set_ttl_based_on_image(metadata, url)
+ [%Oban.Job{scheduled_at: scheduled_at}] = all_enqueued()
- {:ok, cache_ttl} = Cachex.ttl(:rich_media_cache, url)
+ timestamp_dt = Timex.parse!(timestamp, "{ISO:Basic:Z}")
- # as there is delay in setting and pulling the data from cache we ignore 1 second
- # make it 2 seconds for flakyness
- assert_in_delta(valid_till * 1000, cache_ttl, 2000)
+ assert DateTime.diff(scheduled_at, timestamp_dt) == valid_till
end
defp construct_s3_url(timestamp, valid_till) do
@@ -71,11 +88,11 @@ defmodule Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrlTest do
defp construct_metadata(timestamp, valid_till, url) do
%{
- image: construct_s3_url(timestamp, valid_till),
- site: "Pleroma",
- title: "Pleroma",
- description: "Pleroma",
- url: url
+ "image" => construct_s3_url(timestamp, valid_till),
+ "site" => "Pleroma",
+ "title" => "Pleroma",
+ "description" => "Pleroma",
+ "url" => url
}
end
end
diff --git a/test/pleroma/web/rich_media/parser/ttl/opengraph_test.exs b/test/pleroma/web/rich_media/parser/ttl/opengraph_test.exs
@@ -0,0 +1,41 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.RichMedia.Parser.TTL.OpengraphTest do
+ use Pleroma.DataCase
+ use Oban.Testing, repo: Pleroma.Repo
+
+ import Mox
+
+ alias Pleroma.UnstubbedConfigMock, as: ConfigMock
+ alias Pleroma.Web.RichMedia.Card
+
+ setup do
+ ConfigMock
+ |> stub_with(Pleroma.Test.StaticConfig)
+
+ clear_config([:rich_media, :enabled], true)
+
+ :ok
+ end
+
+ test "OpenGraph TTL value is honored" do
+ url = "https://reddit.com/r/somepost"
+
+ Tesla.Mock.mock(fn
+ %{
+ method: :get,
+ url: ^url
+ } ->
+ %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/reddit.html")}
+
+ %{method: :head} ->
+ %Tesla.Env{status: 200}
+ end)
+
+ Card.get_or_backfill_by_url(url)
+
+ assert_enqueued(worker: Pleroma.Workers.RichMediaExpirationWorker, args: %{"url" => url})
+ end
+end
diff --git a/test/pleroma/web/rich_media/parser_test.exs b/test/pleroma/web/rich_media/parser_test.exs
@@ -3,95 +3,26 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.RichMedia.ParserTest do
- use ExUnit.Case, async: true
+ use Pleroma.DataCase
alias Pleroma.Web.RichMedia.Parser
- setup do
- Tesla.Mock.mock(fn
- %{
- method: :get,
- url: "http://example.com/ogp"
- } ->
- %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/ogp.html")}
-
- %{
- method: :get,
- url: "http://example.com/non-ogp"
- } ->
- %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/non_ogp_embed.html")}
-
- %{
- method: :get,
- url: "http://example.com/ogp-missing-title"
- } ->
- %Tesla.Env{
- status: 200,
- body: File.read!("test/fixtures/rich_media/ogp-missing-title.html")
- }
-
- %{
- method: :get,
- url: "http://example.com/twitter-card"
- } ->
- %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/twitter_card.html")}
-
- %{
- method: :get,
- url: "http://example.com/oembed"
- } ->
- %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/oembed.html")}
-
- %{
- method: :get,
- url: "http://example.com/oembed.json"
- } ->
- %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/oembed.json")}
-
- %{method: :get, url: "http://example.com/empty"} ->
- %Tesla.Env{status: 200, body: "hello"}
-
- %{method: :get, url: "http://example.com/malformed"} ->
- %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/malformed-data.html")}
-
- %{method: :get, url: "http://example.com/error"} ->
- {:error, :overload}
-
- %{
- method: :head,
- url: "http://example.com/huge-page"
- } ->
- %Tesla.Env{
- status: 200,
- headers: [{"content-length", "2000001"}, {"content-type", "text/html"}]
- }
-
- %{
- method: :head,
- url: "http://example.com/pdf-file"
- } ->
- %Tesla.Env{
- status: 200,
- headers: [{"content-length", "1000000"}, {"content-type", "application/pdf"}]
- }
-
- %{method: :head} ->
- %Tesla.Env{status: 404, body: "", headers: []}
- end)
+ import Tesla.Mock
- :ok
+ setup do
+ mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
end
test "returns error when no metadata present" do
- assert {:error, _} = Parser.parse("http://example.com/empty")
+ assert {:error, _} = Parser.parse("https://example.com/empty")
end
test "doesn't just add a title" do
- assert {:error, {:invalid_metadata, _}} = Parser.parse("http://example.com/non-ogp")
+ assert {:error, {:invalid_metadata, _}} = Parser.parse("https://example.com/non-ogp")
end
test "parses ogp" do
- assert Parser.parse("http://example.com/ogp") ==
+ assert Parser.parse("https://example.com/ogp") ==
{:ok,
%{
"image" => "http://ia.media-imdb.com/images/rock.jpg",
@@ -99,12 +30,12 @@ defmodule Pleroma.Web.RichMedia.ParserTest do
"description" =>
"Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer.",
"type" => "video.movie",
- "url" => "http://example.com/ogp"
+ "url" => "https://example.com/ogp"
}}
end
test "falls back to <title> when ogp:title is missing" do
- assert Parser.parse("http://example.com/ogp-missing-title") ==
+ assert Parser.parse("https://example.com/ogp-missing-title") ==
{:ok,
%{
"image" => "http://ia.media-imdb.com/images/rock.jpg",
@@ -112,12 +43,12 @@ defmodule Pleroma.Web.RichMedia.ParserTest do
"description" =>
"Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer.",
"type" => "video.movie",
- "url" => "http://example.com/ogp-missing-title"
+ "url" => "https://example.com/ogp-missing-title"
}}
end
test "parses twitter card" do
- assert Parser.parse("http://example.com/twitter-card") ==
+ assert Parser.parse("https://example.com/twitter-card") ==
{:ok,
%{
"card" => "summary",
@@ -125,12 +56,12 @@ defmodule Pleroma.Web.RichMedia.ParserTest do
"image" => "https://farm6.staticflickr.com/5510/14338202952_93595258ff_z.jpg",
"title" => "Small Island Developing States Photo Submission",
"description" => "View the album on Flickr.",
- "url" => "http://example.com/twitter-card"
+ "url" => "https://example.com/twitter-card"
}}
end
test "parses OEmbed and filters HTML tags" do
- assert Parser.parse("http://example.com/oembed") ==
+ assert Parser.parse("https://example.com/oembed") ==
{:ok,
%{
"author_name" => "\u202E\u202D\u202Cbees\u202C",
@@ -150,7 +81,7 @@ defmodule Pleroma.Web.RichMedia.ParserTest do
"thumbnail_width" => 150,
"title" => "Bacon Lollys",
"type" => "photo",
- "url" => "http://example.com/oembed",
+ "url" => "https://example.com/oembed",
"version" => "1.0",
"web_page" => "https://www.flickr.com/photos/bees/2362225867/",
"web_page_short_url" => "https://flic.kr/p/4AK2sc",
@@ -159,18 +90,41 @@ defmodule Pleroma.Web.RichMedia.ParserTest do
end
test "rejects invalid OGP data" do
- assert {:error, _} = Parser.parse("http://example.com/malformed")
+ assert {:error, _} = Parser.parse("https://example.com/malformed")
end
test "returns error if getting page was not successful" do
- assert {:error, :overload} = Parser.parse("http://example.com/error")
+ assert {:error, :overload} = Parser.parse("https://example.com/error")
end
test "does a HEAD request to check if the body is too large" do
- assert {:error, :body_too_large} = Parser.parse("http://example.com/huge-page")
+ assert {:error, :body_too_large} = Parser.parse("https://example.com/huge-page")
end
test "does a HEAD request to check if the body is html" do
- assert {:error, {:content_type, _}} = Parser.parse("http://example.com/pdf-file")
+ assert {:error, {:content_type, _}} = Parser.parse("https://example.com/pdf-file")
+ end
+
+ test "refuses to crawl incomplete URLs" do
+ url = "example.com/ogp"
+ assert :error == Parser.parse(url)
+ end
+
+ test "refuses to crawl malformed URLs" do
+ url = "example.com[]/ogp"
+ assert :error == Parser.parse(url)
+ end
+
+ test "refuses to crawl URLs of private network from posts" do
+ [
+ "http://127.0.0.1:4000/notice/9kCP7VNyPJXFOXDrgO",
+ "https://10.111.10.1/notice/9kCP7V",
+ "https://172.16.32.40/notice/9kCP7V",
+ "https://192.168.10.40/notice/9kCP7V",
+ "https://pleroma.local/notice/9kCP7V"
+ ]
+ |> Enum.each(fn url ->
+ assert :error == Parser.parse(url)
+ end)
end
end
diff --git a/test/pleroma/web/twitter_api/remote_follow_controller_test.exs b/test/pleroma/web/twitter_api/remote_follow_controller_test.exs
@@ -137,7 +137,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do
|> html_response(200)
assert response =~ "Error fetching user"
- end) =~ "Object has been deleted"
+ end) =~ ":not_found"
end
end
diff --git a/test/pleroma/web/web_finger/web_finger_controller_test.exs b/test/pleroma/web/web_finger/web_finger_controller_test.exs
@@ -23,8 +23,15 @@ defmodule Pleroma.Web.WebFinger.WebFingerControllerTest do
assert response.status == 200
- assert response.resp_body ==
- ~s(<?xml version="1.0" encoding="UTF-8"?><XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><Link rel="lrdd" template="#{Pleroma.Web.Endpoint.url()}/.well-known/webfinger?resource={uri}" type="application/xrd+xml" /></XRD>)
+ response_xml =
+ response.resp_body
+ |> Floki.parse_document!(html_parser: Floki.HTMLParser.Mochiweb, attributes_as_maps: true)
+
+ expected_xml =
+ ~s(<?xml version="1.0" encoding="UTF-8"?><XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><Link rel="lrdd" template="#{Pleroma.Web.Endpoint.url()}/.well-known/webfinger?resource={uri}" type="application/xrd+xml" /></XRD>)
+ |> Floki.parse_document!(html_parser: Floki.HTMLParser.Mochiweb, attributes_as_maps: true)
+
+ assert match?(^response_xml, expected_xml)
end
test "Webfinger JRD" do
@@ -48,12 +55,7 @@ defmodule Pleroma.Web.WebFinger.WebFingerControllerTest do
]
end
- test "reach user on tld, while pleroma is runned on subdomain" do
- Pleroma.Web.Endpoint.config_change(
- [{Pleroma.Web.Endpoint, url: [host: "sub.example.com"]}],
- []
- )
-
+ test "reach user on tld, while pleroma is running on subdomain" do
clear_config([Pleroma.Web.Endpoint, :url, :host], "sub.example.com")
clear_config([Pleroma.Web.WebFinger, :domain], "example.com")
@@ -68,13 +70,6 @@ defmodule Pleroma.Web.WebFinger.WebFingerControllerTest do
assert response["subject"] == "acct:#{user.nickname}@example.com"
assert response["aliases"] == ["https://sub.example.com/users/#{user.nickname}"]
-
- on_exit(fn ->
- Pleroma.Web.Endpoint.config_change(
- [{Pleroma.Web.Endpoint, url: [host: "localhost"]}],
- []
- )
- end)
end
test "it returns 404 when user isn't found (JSON)" do
diff --git a/test/pleroma/workers/remote_fetcher_worker_test.exs b/test/pleroma/workers/remote_fetcher_worker_test.exs
@@ -0,0 +1,69 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Workers.RemoteFetcherWorkerTest do
+ use Pleroma.DataCase
+ use Oban.Testing, repo: Pleroma.Repo
+
+ alias Pleroma.Workers.RemoteFetcherWorker
+
+ @deleted_object_one "https://deleted-404.example.com/"
+ @deleted_object_two "https://deleted-410.example.com/"
+ @unauthorized_object "https://unauthorized.example.com/"
+ @depth_object "https://depth.example.com/"
+
+ describe "RemoteFetcherWorker" do
+ setup do
+ Tesla.Mock.mock(fn
+ %{method: :get, url: @deleted_object_one} ->
+ %Tesla.Env{
+ status: 404
+ }
+
+ %{method: :get, url: @deleted_object_two} ->
+ %Tesla.Env{
+ status: 410
+ }
+
+ %{method: :get, url: @unauthorized_object} ->
+ %Tesla.Env{
+ status: 403
+ }
+
+ %{method: :get, url: @depth_object} ->
+ %Tesla.Env{
+ status: 200
+ }
+ end)
+ end
+
+ test "does not requeue a deleted object" do
+ assert {:discard, _} =
+ RemoteFetcherWorker.perform(%Oban.Job{
+ args: %{"op" => "fetch_remote", "id" => @deleted_object_one}
+ })
+
+ assert {:discard, _} =
+ RemoteFetcherWorker.perform(%Oban.Job{
+ args: %{"op" => "fetch_remote", "id" => @deleted_object_two}
+ })
+ end
+
+ test "does not requeue an unauthorized object" do
+ assert {:discard, _} =
+ RemoteFetcherWorker.perform(%Oban.Job{
+ args: %{"op" => "fetch_remote", "id" => @unauthorized_object}
+ })
+ end
+
+ test "does not requeue an object that exceeded depth" do
+ clear_config([:instance, :federation_incoming_replies_max_depth], 0)
+
+ assert {:discard, _} =
+ RemoteFetcherWorker.perform(%Oban.Job{
+ args: %{"op" => "fetch_remote", "id" => @depth_object, "depth" => 1}
+ })
+ end
+ end
+end
diff --git a/test/support/cachex_proxy.ex b/test/support/cachex_proxy.ex
@@ -27,9 +27,15 @@ defmodule Pleroma.CachexProxy do
defdelegate fetch!(cache, key, func), to: Cachex
@impl true
+ defdelegate fetch(cache, key, func), to: Cachex
+
+ @impl true
defdelegate expire_at(cache, str, num), to: Cachex
@impl true
+ defdelegate expire(cache, str, num), to: Cachex
+
+ @impl true
defdelegate exists?(cache, key), to: Cachex
@impl true
diff --git a/test/support/helpers.ex b/test/support/helpers.ex
@@ -10,6 +10,39 @@ defmodule Pleroma.Tests.Helpers do
require Logger
+ @doc "Accepts two URLs/URIs and sorts the query parameters before comparing"
+ def uri_equal?(a, b) do
+ a_sorted = uri_query_sort(a)
+ b_sorted = uri_query_sort(b)
+
+ match?(^a_sorted, b_sorted)
+ end
+
+ @doc "Accepts a URL/URI and sorts the query parameters"
+ def uri_query_sort(uri) do
+ parsed = URI.parse(uri)
+
+ sorted_query =
+ String.split(parsed.query, "&")
+ |> Enum.sort()
+ |> Enum.join("&")
+
+ parsed
+ |> Map.put(:query, sorted_query)
+ |> URI.to_string()
+ end
+
+ @doc "Returns the value of the specified query parameter for the provided URL"
+ def get_query_parameter(url, param) do
+ url
+ |> URI.parse()
+ |> Map.get(:query)
+ |> URI.query_decoder()
+ |> Enum.to_list()
+ |> Enum.into(%{}, fn {x, y} -> {x, y} end)
+ |> Map.get(param)
+ end
+
defmacro clear_config(config_path) do
quote do
clear_config(unquote(config_path)) do
diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex
@@ -178,7 +178,7 @@ defmodule HttpRequestMock do
end
def get(
- "https://social.heldscal.la/.well-known/webfinger?resource=nonexistant@social.heldscal.la",
+ "https://social.heldscal.la/.well-known/webfinger?resource=nonexistent@social.heldscal.la",
_,
_,
[{"accept", "application/xrd+xml,application/jrd+json"}]
@@ -186,7 +186,7 @@ defmodule HttpRequestMock do
{:ok,
%Tesla.Env{
status: 200,
- body: File.read!("test/fixtures/tesla_mock/nonexistant@social.heldscal.la.xml")
+ body: File.read!("test/fixtures/tesla_mock/nonexistent@social.heldscal.la.xml")
}}
end
@@ -1059,7 +1059,7 @@ defmodule HttpRequestMock do
}}
end
- def get("http://example.com/malformed", _, _, _) do
+ def get("https://example.com/malformed", _, _, _) do
{:ok,
%Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/malformed-data.html")}}
end
@@ -1464,6 +1464,63 @@ defmodule HttpRequestMock do
}}
end
+ def get("https://google.com/", _, _, _) do
+ {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/google.html")}}
+ end
+
+ def get("https://yahoo.com/", _, _, _) do
+ {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/yahoo.html")}}
+ end
+
+ def get("https://example.com/error", _, _, _), do: {:error, :overload}
+
+ def get("https://example.com/ogp-missing-title", _, _, _) do
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/rich_media/ogp-missing-title.html")
+ }}
+ end
+
+ def get("https://example.com/oembed", _, _, _) do
+ {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/oembed.html")}}
+ end
+
+ def get("https://example.com/oembed.json", _, _, _) do
+ {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/oembed.json")}}
+ end
+
+ def get("https://example.com/twitter-card", _, _, _) do
+ {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/twitter_card.html")}}
+ end
+
+ def get("https://example.com/non-ogp", _, _, _) do
+ {:ok,
+ %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/non_ogp_embed.html")}}
+ end
+
+ def get("https://example.com/empty", _, _, _) do
+ {:ok, %Tesla.Env{status: 200, body: "hello"}}
+ end
+
+ def get("https://friends.grishka.me/posts/54642", _, _, _) do
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/tesla_mock/smithereen_non_anonymous_poll.json"),
+ headers: activitypub_object_headers()
+ }}
+ end
+
+ def get("https://friends.grishka.me/users/1", _, _, _) do
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/tesla_mock/smithereen_user.json"),
+ headers: activitypub_object_headers()
+ }}
+ end
+
def get(url, query, body, headers) do
{:error,
"Mock response not implemented for GET #{inspect(url)}, #{query}, #{inspect(body)}, #{inspect(headers)}"}
@@ -1537,14 +1594,41 @@ defmodule HttpRequestMock do
# Most of the rich media mocks are missing HEAD requests, so we just return 404.
@rich_media_mocks [
+ "https://example.com/empty",
+ "https://example.com/error",
+ "https://example.com/malformed",
+ "https://example.com/non-ogp",
+ "https://example.com/oembed",
+ "https://example.com/oembed.json",
"https://example.com/ogp",
"https://example.com/ogp-missing-data",
- "https://example.com/twitter-card"
+ "https://example.com/ogp-missing-title",
+ "https://example.com/twitter-card",
+ "https://google.com/",
+ "https://pleroma.local/notice/9kCP7V",
+ "https://yahoo.com/"
]
+
def head(url, _query, _body, _headers) when url in @rich_media_mocks do
{:ok, %Tesla.Env{status: 404, body: ""}}
end
+ def head("https://example.com/pdf-file", _, _, _) do
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ headers: [{"content-length", "1000000"}, {"content-type", "application/pdf"}]
+ }}
+ end
+
+ def head("https://example.com/huge-page", _, _, _) do
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ headers: [{"content-length", "2000001"}, {"content-type", "text/html"}]
+ }}
+ end
+
def head(url, query, body, headers) do
{:error,
"Mock response not implemented for HEAD #{inspect(url)}, #{query}, #{inspect(body)}, #{inspect(headers)}"}
diff --git a/test/support/null_cache.ex b/test/support/null_cache.ex
@@ -29,6 +29,9 @@ defmodule Pleroma.NullCache do
end
@impl true
+ def fetch(_, key, func), do: func.(key)
+
+ @impl true
def get_and_update(_, _, func) do
func.(nil)
end
@@ -37,6 +40,9 @@ defmodule Pleroma.NullCache do
def expire_at(_, _, _), do: {:ok, true}
@impl true
+ def expire(_, _, _), do: {:ok, true}
+
+ @impl true
def exists?(_, _), do: {:ok, false}
@impl true
diff --git a/test/test_helper.exs b/test/test_helper.exs
@@ -4,8 +4,16 @@
Code.put_compiler_option(:warnings_as_errors, true)
+ExUnit.configure(max_cases: System.schedulers_online())
+
ExUnit.start(exclude: [:federated, :erratic])
+if match?({:unix, :darwin}, :os.type()) do
+ excluded = ExUnit.configuration() |> Keyword.get(:exclude, [])
+ excluded = excluded ++ [:skip_darwin]
+ ExUnit.configure(exclude: excluded)
+end
+
Ecto.Adapters.SQL.Sandbox.mode(Pleroma.Repo, :manual)
Mox.defmock(Pleroma.ReverseProxy.ClientMock, for: Pleroma.ReverseProxy.Client)
diff --git a/uploads/.gitignore b/uploads/.gitignore
@@ -1,3 +0,0 @@
-# Git will ignore everything in this directory except this file.
-*
-!.gitignore