Page MenuHomePhorge

No OneTemporary

Authored By
Unknown
Size
747 KB
Referenced Files
None
Subscribers
None
This file is larger than 256 KB, so syntax highlighting was skipped.
diff --git a/changelog.d/card-endpoint.remove b/changelog.d/card-endpoint.remove
new file mode 100644
index 000000000..e09a24cf7
--- /dev/null
+++ 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/rich_media_refactor.change b/changelog.d/rich_media_refactor.change
new file mode 100644
index 000000000..c0d4e3b0a
--- /dev/null
+++ 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/config/config.exs b/config/config.exs
index 32c8509be..b69044a2b 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -1,915 +1,920 @@
# .i;;;;i.
# iYcviii;vXY:
# .YXi .i1c.
# .YC. . in7.
# .vc. ...... ;1c.
# i7, .. .;1;
# i7, .. ... .Y1i
# ,7v .6MMM@; .YX,
# .7;. ..IMMMMMM1 :t7.
# .;Y. ;$MMMMMM9. :tc.
# vY. .. .nMMM@MMU. ;1v.
# i7i ... .#MM@M@C. .....:71i
# it: .... $MMM@9;.,i;;;i,;tti
# :t7. ..... 0MMMWv.,iii:::,,;St.
# .nC. ..... IMMMQ..,::::::,.,czX.
# .ct: ....... .ZMMMI..,:::::::,,:76Y.
# c2: ......,i..Y$M@t..:::::::,,..inZY
# vov ......:ii..c$MBc..,,,,,,,,,,..iI9i
# i9Y ......iii:..7@MA,..,,,,,,,,,....;AA:
# iIS. ......:ii::..;@MI....,............;Ez.
# .I9. ......:i::::...8M1..................C0z.
# .z9; ......:i::::,.. .i:...................zWX.
# vbv ......,i::::,,. ................. :AQY
# c6Y. .,...,::::,,..:t0@@QY. ................ :8bi
# :6S. ..,,...,:::,,,..EMMMMMMI. ............... .;bZ,
# :6o, .,,,,..:::,,,..i#MMMMMM#v................. YW2.
# .n8i ..,,,,,,,::,,,,.. tMMMMM@C:.................. .1Wn
# 7Uc. .:::,,,,,::,,,,.. i1t;,..................... .UEi
# 7C...::::::::::::,,,,.. .................... vSi.
# ;1;...,,::::::,......... .................. Yz:
# v97,......... .voC.
# izAotX7777777777777777777777777777777777777777Y7n92:
# .;CoIIIIIUAA666666699999ZZZZZZZZZZZZZZZZZZZZ6ov.
#
# !!! ATTENTION !!!
# DO NOT EDIT THIS FILE! THIS FILE CONTAINS THE DEFAULT VALUES FOR THE CON-
# FIGURATION! EDIT YOUR SECRET FILE (either prod.secret.exs, dev.secret.exs).
#
# This file is responsible for configuring your application
# and its dependencies with the aid of the Config module.
#
# This configuration file is loaded before any dependency and
# is restricted to this project.
import Config
# General application configuration
config :pleroma, ecto_repos: [Pleroma.Repo]
config :pleroma, Pleroma.Repo,
telemetry_event: [Pleroma.Repo.Instrumenter],
migration_lock: nil
config :pleroma, Pleroma.Captcha,
enabled: true,
seconds_valid: 300,
method: Pleroma.Captcha.Native
config :pleroma, Pleroma.Captcha.Kocaptcha, endpoint: "https://captcha.kotobank.ch"
# Upload configuration
config :pleroma, Pleroma.Upload,
uploader: Pleroma.Uploaders.Local,
filters: [Pleroma.Upload.Filter.Dedupe],
link_name: false,
proxy_remote: false,
filename_display_max_length: 30,
default_description: nil,
base_url: nil
config :pleroma, Pleroma.Uploaders.Local, uploads: "uploads"
config :pleroma, Pleroma.Uploaders.S3,
bucket: nil,
bucket_namespace: nil,
truncated_namespace: nil,
streaming_enabled: true
config :ex_aws, :s3,
# host: "s3.wasabisys.com", # required if not Amazon AWS
access_key_id: nil,
secret_access_key: nil,
# region: "us-east-1", # may be required for Amazon AWS
scheme: "https://"
config :pleroma, :emoji,
shortcode_globs: ["/emoji/custom/**/*.png"],
pack_extensions: [".png", ".gif"],
groups: [
Custom: ["/emoji/*.png", "/emoji/**/*.png"]
],
default_manifest: "https://git.pleroma.social/pleroma/emoji-index/raw/master/index.json",
shared_pack_cache_seconds_per_file: 60
config :pleroma, :uri_schemes,
valid_schemes: [
"https",
"http",
"dat",
"dweb",
"gopher",
"hyper",
"ipfs",
"ipns",
"irc",
"ircs",
"magnet",
"mailto",
"mumble",
"ssb",
"xmpp"
]
# Configures the endpoint
config :pleroma, Pleroma.Web.Endpoint,
url: [host: "localhost"],
http: [
ip: {127, 0, 0, 1}
],
protocol: "https",
secret_key_base: "aK4Abxf29xU9TTDKre9coZPUgevcVCFQJe/5xP/7Lt4BEif6idBIbjupVbOrbKxl",
live_view: [signing_salt: "U5ELgdEwTD3n1+D5s0rY0AMg8/y1STxZ3Zvsl3bWh+oBcGrYdil0rXqPMRd3Glcq"],
signing_salt: "CqaoopA2",
render_errors: [view: Pleroma.Web.ErrorView, accepts: ~w(json)],
pubsub_server: Pleroma.PubSub,
secure_cookie_flag: true,
extra_cookie_attrs: [
"SameSite=Lax"
]
# Configures Elixir's Logger
config :logger, :console,
level: :debug,
format: "\n$time $metadata[$level] $message\n",
metadata: [:request_id]
config :logger, :ex_syslogger,
level: :debug,
ident: "pleroma",
format: "$metadata[$level] $message",
metadata: [:request_id]
config :mime, :types, %{
"application/xml" => ["xml"],
"application/xrd+xml" => ["xrd+xml"],
"application/jrd+json" => ["jrd+json"],
"application/activity+json" => ["activity+json"],
"application/ld+json" => ["activity+json"]
}
config :tesla, adapter: Tesla.Adapter.Hackney
# Configures http settings, upstream proxy etc.
config :pleroma, :http,
proxy_url: nil,
send_user_agent: true,
user_agent: :default,
adapter: []
config :pleroma, :instance,
name: "Pleroma",
email: "example@example.com",
notify_email: "noreply@example.com",
description: "Pleroma: An efficient and flexible fediverse server",
short_description: "",
background_image: "/images/city.jpg",
instance_thumbnail: "/instance/thumbnail.jpeg",
favicon: "/favicon.png",
limit: 5_000,
description_limit: 5_000,
remote_limit: 100_000,
upload_limit: 16_000_000,
avatar_upload_limit: 2_000_000,
background_upload_limit: 4_000_000,
banner_upload_limit: 4_000_000,
poll_limits: %{
max_options: 20,
max_option_chars: 200,
min_expiration: 0,
max_expiration: 365 * 24 * 60 * 60
},
registrations_open: true,
invites_enabled: false,
account_activation_required: false,
account_approval_required: false,
federating: true,
federation_incoming_replies_max_depth: 100,
federation_reachability_timeout_days: 7,
allow_relay: true,
public: true,
quarantined_instances: [],
static_dir: "instance/static/",
allowed_post_formats: [
"text/plain",
"text/html",
"text/markdown",
"text/bbcode"
],
autofollowed_nicknames: [],
autofollowing_nicknames: [],
max_pinned_statuses: 1,
attachment_links: false,
max_report_comment_size: 1000,
report_strip_status: true,
safe_dm_mentions: false,
healthcheck: false,
remote_post_retention_days: 90,
skip_thread_containment: true,
limit_to_local_content: :unauthenticated,
user_bio_length: 5000,
user_name_length: 100,
max_account_fields: 10,
max_remote_account_fields: 20,
account_field_name_length: 512,
account_field_value_length: 2048,
registration_reason_length: 500,
external_user_synchronization: true,
extended_nickname_format: true,
cleanup_attachments: false,
multi_factor_authentication: [
totp: [
# digits 6 or 8
digits: 6,
period: 30
],
backup_codes: [
number: 5,
length: 16
]
],
show_reactions: true,
password_reset_token_validity: 60 * 60 * 24,
profile_directory: true,
admin_privileges: [
:users_read,
:users_manage_invites,
:users_manage_activation_state,
:users_manage_tags,
:users_manage_credentials,
:users_delete,
:messages_read,
:messages_delete,
:instances_delete,
:reports_manage_reports,
:moderation_log_read,
:announcements_manage_announcements,
:emoji_manage_emoji,
:statistics_read
],
moderator_privileges: [:messages_delete, :reports_manage_reports],
max_endorsed_users: 20,
birthday_required: false,
birthday_min_age: 0,
max_media_attachments: 1_000
config :pleroma, :welcome,
direct_message: [
enabled: false,
sender_nickname: nil,
message: nil
],
chat_message: [
enabled: false,
sender_nickname: nil,
message: nil
],
email: [
enabled: false,
sender: nil,
subject: "Welcome to <%= instance_name %>",
html: "Welcome to <%= instance_name %>",
text: "Welcome to <%= instance_name %>"
]
config :pleroma, :feed,
post_title: %{
max_length: 100,
omission: "..."
}
config :pleroma, :markup,
# XXX - unfortunately, inline images must be enabled by default right now, because
# of custom emoji. Issue #275 discusses defanging that somehow.
allow_inline_images: true,
allow_headings: false,
allow_tables: false,
allow_fonts: false,
scrub_policy: [
Pleroma.HTML.Scrubber.Default,
Pleroma.HTML.Transform.MediaProxy
]
config :pleroma, :frontend_configurations,
pleroma_fe: %{
alwaysShowSubjectInput: true,
background: "/images/city.jpg",
collapseMessageWithSubject: false,
disableChat: false,
greentext: false,
hideFilteredStatuses: false,
hideMutedPosts: false,
hidePostStats: false,
hideSitename: false,
hideUserStats: false,
loginMethod: "password",
logo: "/static/logo.svg",
logoMargin: ".1em",
logoMask: true,
minimalScopesMode: false,
noAttachmentLinks: false,
nsfwCensorImage: "",
postContentType: "text/plain",
redirectRootLogin: "/main/friends",
redirectRootNoLogin: "/main/all",
scopeCopy: true,
sidebarRight: false,
showFeaturesPanel: true,
showInstanceSpecificPanel: false,
subjectLineBehavior: "email",
theme: "pleroma-dark",
webPushNotifications: false
}
config :pleroma, :assets,
mascots: [
pleroma_fox_tan: %{
url: "/images/pleroma-fox-tan-smol.png",
mime_type: "image/png"
},
pleroma_fox_tan_shy: %{
url: "/images/pleroma-fox-tan-shy.png",
mime_type: "image/png"
}
],
default_mascot: :pleroma_fox_tan
config :pleroma, :manifest,
icons: [
%{
src: "/static/logo.svg",
sizes: "144x144",
purpose: "any",
type: "image/svg+xml"
}
],
theme_color: "#282c37",
background_color: "#191b22"
config :pleroma, :activitypub,
unfollow_blocked: true,
outgoing_blocks: true,
blockers_visible: true,
follow_handshake_timeout: 500,
note_replies_output_limit: 5,
sign_object_fetches: true,
authorized_fetch_mode: false
config :pleroma, :streamer,
workers: 3,
overflow_workers: 2
config :pleroma, :user, deny_follow_blocked: true
config :pleroma, :mrf_normalize_markup, scrub_policy: Pleroma.HTML.Scrubber.Default
config :pleroma, :mrf_rejectnonpublic,
allow_followersonly: false,
allow_direct: false
config :pleroma, :mrf_hellthread,
delist_threshold: 10,
reject_threshold: 20
config :pleroma, :mrf_simple,
media_removal: [],
media_nsfw: [],
federated_timeline_removal: [],
report_removal: [],
reject: [],
followers_only: [],
accept: [],
avatar_removal: [],
banner_removal: [],
reject_deletes: []
config :pleroma, :mrf_keyword,
reject: [],
federated_timeline_removal: [],
replace: []
config :pleroma, :mrf_emoji,
remove_url: [],
remove_shortcode: [],
federated_timeline_removal_url: [],
federated_timeline_removal_shortcode: []
config :pleroma, :mrf_hashtag,
sensitive: ["nsfw"],
reject: [],
federated_timeline_removal: []
config :pleroma, :mrf_subchain, match_actor: %{}
config :pleroma, :mrf_activity_expiration, days: 365
config :pleroma, :mrf_vocabulary,
accept: [],
reject: []
# threshold of 7 days
config :pleroma, :mrf_object_age,
threshold: 604_800,
actions: [:delist, :strip_followers]
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: [],
ignore_tld: ["local", "localdomain", "lan"],
parsers: [
Pleroma.Web.RichMedia.Parsers.TwitterCard,
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,
invalidation: [
enabled: false,
provider: Pleroma.Web.MediaProxy.Invalidation.Script
],
proxy_opts: [
redirect_on_failure: false,
max_body_length: 25 * 1_048_576,
# Note: max_read_duration defaults to Pleroma.ReverseProxy.max_read_duration_default/1
max_read_duration: 30_000,
http: [
follow_redirect: true,
pool: :media
]
],
whitelist: []
config :pleroma, Pleroma.Web.MediaProxy.Invalidation.Http,
method: :purge,
headers: [],
options: []
config :pleroma, Pleroma.Web.MediaProxy.Invalidation.Script,
script_path: nil,
url_format: nil
# Note: media preview proxy depends on media proxy to be enabled
config :pleroma, :media_preview_proxy,
enabled: false,
thumbnail_max_width: 600,
thumbnail_max_height: 600,
image_quality: 85,
min_content_length: 100 * 1024
config :pleroma, :shout,
enabled: true,
limit: 5_000
config :phoenix, :format_encoders, json: Jason, "activity+json": Jason
config :phoenix, :json_library, Jason
config :phoenix, :filter_parameters, ["password", "confirm"]
config :pleroma, :gopher,
enabled: false,
ip: {0, 0, 0, 0},
port: 9999
config :pleroma, Pleroma.Web.Metadata,
providers: [
Pleroma.Web.Metadata.Providers.OpenGraph,
Pleroma.Web.Metadata.Providers.TwitterCard
],
unfurl_nsfw: false
config :pleroma, Pleroma.Web.Preload,
providers: [
Pleroma.Web.Preload.Providers.Instance
]
config :pleroma, :http_security,
enabled: true,
sts: false,
sts_max_age: 31_536_000,
ct_max_age: 2_592_000,
referrer_policy: "same-origin"
config :cors_plug,
max_age: 86_400,
methods: ["POST", "PUT", "DELETE", "GET", "PATCH", "OPTIONS"],
expose: [
"Link",
"X-RateLimit-Reset",
"X-RateLimit-Limit",
"X-RateLimit-Remaining",
"X-Request-Id",
"Idempotency-Key"
],
credentials: true,
headers: ["Authorization", "Content-Type", "Idempotency-Key"]
config :pleroma, Pleroma.User,
restricted_nicknames: [
".well-known",
"~",
"about",
"activities",
"api",
"auth",
"check_password",
"dev",
"friend-requests",
"inbox",
"internal",
"main",
"media",
"nodeinfo",
"notice",
"oauth",
"objects",
"ostatus_subscribe",
"pleroma",
"proxy",
"push",
"registration",
"relay",
"settings",
"status",
"tag",
"user-search",
"user_exists",
"users",
"web",
"verify_credentials",
"update_credentials",
"relationships",
"search",
"confirmation_resend",
"mfa"
],
email_blacklist: []
config :pleroma, Oban,
repo: Pleroma.Repo,
log: false,
queues: [
activity_expiration: 10,
token_expiration: 5,
filter_expiration: 1,
backup: 1,
federator_incoming: 5,
federator_outgoing: 5,
ingestion_queue: 50,
web_push: 50,
mailer: 10,
transmogrifier: 20,
scheduled_activities: 10,
poll_notifications: 10,
background: 5,
remote_fetcher: 2,
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: [
{"0 0 * * 0", Pleroma.Workers.Cron.DigestEmailsWorker},
{"0 0 * * *", Pleroma.Workers.Cron.NewUsersDigestWorker}
]
config :pleroma, :workers,
retries: [
federator_incoming: 5,
federator_outgoing: 5,
search_indexing: 2
]
config :pleroma, Pleroma.Formatter,
class: false,
rel: "ugc",
new_window: false,
truncate: false,
strip_prefix: false,
extra: true,
validate_tld: :no_scheme
config :pleroma, :ldap,
enabled: System.get_env("LDAP_ENABLED") == "true",
host: System.get_env("LDAP_HOST") || "localhost",
port: String.to_integer(System.get_env("LDAP_PORT") || "389"),
ssl: System.get_env("LDAP_SSL") == "true",
sslopts: [],
tls: System.get_env("LDAP_TLS") == "true",
tlsopts: [],
base: System.get_env("LDAP_BASE") || "dc=example,dc=com",
uid: System.get_env("LDAP_UID") || "cn"
oauth_consumer_strategies =
System.get_env("OAUTH_CONSUMER_STRATEGIES")
|> to_string()
|> String.split()
|> Enum.map(&hd(String.split(&1, ":")))
ueberauth_providers =
for strategy <- oauth_consumer_strategies do
strategy_module_name = "Elixir.Ueberauth.Strategy.#{String.capitalize(strategy)}"
strategy_module = String.to_atom(strategy_module_name)
{String.to_atom(strategy), {strategy_module, [callback_params: ["state"]]}}
end
config :ueberauth,
Ueberauth,
base_path: "/oauth",
providers: ueberauth_providers
config :pleroma, :auth, oauth_consumer_strategies: oauth_consumer_strategies
config :pleroma, Pleroma.Emails.Mailer, adapter: Swoosh.Adapters.Sendmail, enabled: false
config :pleroma, Pleroma.Emails.UserEmail,
logo: nil,
styling: %{
link_color: "#d8a070",
background_color: "#2C3645",
content_background_color: "#1B2635",
header_color: "#d8a070",
text_color: "#b9b9ba",
text_muted_color: "#b9b9ba"
}
config :pleroma, Pleroma.Emails.NewUsersDigestEmail, enabled: false
config :pleroma, Pleroma.PromEx,
disabled: false,
manual_metrics_start_delay: :no_delay,
drop_metrics_groups: [],
grafana: [
host: System.get_env("GRAFANA_HOST", "http://localhost:3000"),
auth_token: System.get_env("GRAFANA_TOKEN"),
upload_dashboards_on_start: false,
folder_name: "BEAM",
annotate_app_lifecycle: true
],
metrics_server: [
port: 4021,
path: "/metrics",
protocol: :http,
pool_size: 5,
cowboy_opts: [],
auth_strategy: :none
],
datasource: "Prometheus"
config :pleroma, Pleroma.ScheduledActivity,
daily_user_limit: 25,
total_user_limit: 300,
enabled: true
config :pleroma, :email_notifications,
digest: %{
active: false,
interval: 7,
inactivity_threshold: 7
}
config :pleroma, :oauth2,
token_expires_in: 3600 * 24 * 365 * 100,
issue_new_refresh_token: true,
clean_expired_tokens: false
config :pleroma, :database, rum_enabled: false
config :pleroma, :features, improved_hashtag_timeline: :auto
config :pleroma, :populate_hashtags_table, fault_rate_allowance: 0.01
config :pleroma, :delete_context_objects, fault_rate_allowance: 0.01
config :pleroma, :env, Mix.env()
config :http_signatures,
adapter: Pleroma.Signature
config :pleroma, :rate_limit,
authentication: {60_000, 15},
timeline: {500, 3},
search: [{1000, 10}, {1000, 30}],
app_account_creation: {1_800_000, 25},
relations_actions: {10_000, 10},
relation_id_action: {60_000, 2},
statuses_actions: {10_000, 15},
status_id_action: {60_000, 3},
password_reset: {1_800_000, 5},
account_confirmation_resend: {8_640_000, 5},
ap_routes: {60_000, 15}
config :pleroma, Pleroma.Workers.PurgeExpiredActivity, enabled: true, min_lifetime: 600
config :pleroma, Pleroma.Web.Plugs.RemoteIp,
enabled: true,
headers: ["x-forwarded-for"],
proxies: [],
reserved: [
"127.0.0.0/8",
"::1/128",
"fc00::/7",
"10.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16"
]
config :pleroma, :static_fe, enabled: false
# Example of frontend configuration
# This example will make us serve the primary frontend from the
# frontends directory within your `:pleroma, :instance, static_dir`.
# e.g., instance/static/frontends/pleroma/develop/
#
# With no frontend configuration, the bundled files from the `static` directory will
# be used.
#
# config :pleroma, :frontends,
# primary: %{"name" => "pleroma-fe", "ref" => "develop"},
# admin: %{"name" => "admin-fe", "ref" => "stable"},
# available: %{...}
config :pleroma, :frontends,
available: %{
"kenoma" => %{
"name" => "kenoma",
"git" => "https://git.pleroma.social/lambadalambda/kenoma",
"build_url" =>
"https://git.pleroma.social/lambadalambda/kenoma/-/jobs/artifacts/${ref}/download?job=build",
"ref" => "master"
},
"pleroma-fe" => %{
"name" => "pleroma-fe",
"git" => "https://git.pleroma.social/pleroma/pleroma-fe",
"build_url" =>
"https://git.pleroma.social/pleroma/pleroma-fe/-/jobs/artifacts/${ref}/download?job=build",
"ref" => "develop"
},
"fedi-fe" => %{
"name" => "fedi-fe",
"git" => "https://git.pleroma.social/pleroma/fedi-fe",
"build_url" =>
"https://git.pleroma.social/pleroma/fedi-fe/-/jobs/artifacts/${ref}/download?job=build_release",
"ref" => "master",
"custom-http-headers" => [
{"service-worker-allowed", "/"}
]
},
"admin-fe" => %{
"name" => "admin-fe",
"git" => "https://git.pleroma.social/pleroma/admin-fe",
"build_url" =>
"https://git.pleroma.social/pleroma/admin-fe/-/jobs/artifacts/${ref}/download?job=build",
"ref" => "develop"
},
"soapbox" => %{
"name" => "soapbox",
"git" => "https://gitlab.com/soapbox-pub/soapbox",
"build_url" =>
"https://gitlab.com/soapbox-pub/soapbox/-/jobs/artifacts/${ref}/download?job=build-production",
"ref" => "v3.0.0-beta.1",
"build_dir" => "static"
},
"glitch-lily" => %{
"name" => "glitch-lily",
"git" => "https://lily-is.land/infra/glitch-lily",
"build_url" =>
"https://lily-is.land/infra/glitch-lily/-/jobs/artifacts/${ref}/download?job=build",
"ref" => "servant",
"build_dir" => "public"
}
}
config :pleroma, :web_cache_ttl,
activity_pub: nil,
activity_pub_question: 30_000
config :pleroma, :modules, runtime_dir: "instance/modules"
config :pleroma, configurable_from_database: false
config :pleroma, Pleroma.Repo,
parameters: [gin_fuzzy_search_limit: "500", jit: "off"],
prepare: :unnamed
config :pleroma, :connections_pool,
reclaim_multiplier: 0.1,
connection_acquisition_wait: 250,
connection_acquisition_retries: 5,
max_connections: 250,
max_idle_time: 30_000,
retry: 0,
connect_timeout: 5_000
config :pleroma, :pools,
federation: [
size: 50,
max_waiting: 10,
recv_timeout: 10_000
],
media: [
size: 50,
max_waiting: 20,
recv_timeout: 15_000
],
upload: [
size: 25,
max_waiting: 5,
recv_timeout: 15_000
],
default: [
size: 10,
max_waiting: 2,
recv_timeout: 5_000
]
config :pleroma, :hackney_pools,
federation: [
max_connections: 50,
timeout: 150_000
],
media: [
max_connections: 50,
timeout: 150_000
],
upload: [
max_connections: 25,
timeout: 300_000
]
config :pleroma, :majic_pool, size: 2
private_instance? = :if_instance_is_private
config :pleroma, :restrict_unauthenticated,
timelines: %{local: private_instance?, federated: private_instance?},
profiles: %{local: private_instance?, remote: private_instance?},
activities: %{local: private_instance?, remote: private_instance?}
config :pleroma, Pleroma.Web.ApiSpec.CastAndValidate, strict: false
config :pleroma, :mrf,
policies: [
Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy,
Pleroma.Web.ActivityPub.MRF.TagPolicy,
Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy
],
transparency: true,
transparency_exclusions: []
config :tzdata, :http_client, Pleroma.HTTP.Tzdata
config :ex_aws, http_client: Pleroma.HTTP.ExAws
config :web_push_encryption, http_client: Pleroma.HTTP.WebPush
config :pleroma, :instances_favicons, enabled: false
config :floki, :html_parser, Floki.HTMLParser.FastHtml
config :pleroma, Pleroma.Web.Auth.Authenticator, Pleroma.Web.Auth.PleromaAuthenticator
config :pleroma, Pleroma.User.Backup,
purge_after_days: 30,
limit_days: 7,
dir: nil,
process_wait_time: 30_000,
process_chunk_size: 100
config :pleroma, ConcurrentLimiter, [
{Pleroma.Web.RichMedia.Helpers, [max_running: 5, max_waiting: 5]},
{Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy, [max_running: 5, max_waiting: 5]},
{Pleroma.Search, [max_running: 30, max_waiting: 50]}
]
config :pleroma, Pleroma.Web.WebFinger, domain: nil, update_nickname_on_user_fetch: true
config :pleroma, Pleroma.Search, module: Pleroma.Search.DatabaseSearch
config :pleroma, Pleroma.Search.Meilisearch,
url: "http://127.0.0.1:7700/",
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/test.exs b/config/test.exs
index 80b01932c..9b4113dd5 100644
--- a/config/test.exs
+++ b/config/test.exs
@@ -1,183 +1,186 @@
import Config
# We don't run a server during test. If one is required,
# you can enable the server option below.
config :pleroma, Pleroma.Web.Endpoint,
http: [port: 4001],
url: [port: 4001],
server: true
# Disable captha for tests
config :pleroma, Pleroma.Captcha,
# It should not be enabled for automatic tests
enabled: false,
# A fake captcha service for tests
method: Pleroma.Captcha.Mock
# Print only warnings and errors during test
config :logger, :console,
level: :warning,
format: "\n[$level] $message\n"
config :pleroma, :auth, oauth_consumer_strategies: []
config :pleroma, Pleroma.Upload,
filters: [],
link_name: false,
default_description: :filename
config :pleroma, Pleroma.Uploaders.Local, uploads: "test/uploads"
config :pleroma, Pleroma.Emails.Mailer, adapter: Swoosh.Adapters.Test, enabled: true
config :pleroma, :instance,
email: "admin@example.com",
notify_email: "noreply@example.com",
skip_thread_containment: false,
federating: false,
external_user_synchronization: false,
static_dir: "test/instance_static/"
config :pleroma, :activitypub, sign_object_fetches: false, follow_handshake_timeout: 0
# Configure your database
config :pleroma, Pleroma.Repo,
adapter: Ecto.Adapters.Postgres,
username: "postgres",
password: "postgres",
database: "pleroma_test",
hostname: System.get_env("DB_HOST") || "localhost",
port: System.get_env("DB_PORT") || "5432",
pool: Ecto.Adapters.SQL.Sandbox,
pool_size: System.schedulers_online() * 2
config :pleroma, :dangerzone, override_repo_pool_size: true
# Reduce hash rounds for testing
config :pleroma, :password, iterations: 1
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: [
totp: [
# digits 6 or 8
digits: 6,
period: 30
],
backup_codes: [
number: 2,
length: 6
]
]
config :web_push_encryption, :vapid_details,
subject: "mailto:administrator@example.com",
public_key:
"BLH1qVhJItRGCfxgTtONfsOKDc9VRAraXw-3NsmjMngWSh7NxOizN6bkuRA7iLTMPS82PjwJAr3UoK9EC1IFrz4",
private_key: "_-XZ0iebPrRfZ_o0-IatTdszYa8VCH1yLN-JauK7HHA"
config :pleroma, Oban, testing: :manual
config :pleroma, Pleroma.ScheduledActivity,
daily_user_limit: 2,
total_user_limit: 3,
enabled: false
config :pleroma, :rate_limit, %{}
config :pleroma, :http_security, report_uri: "https://endpoint.com"
config :pleroma, :http, send_user_agent: false
rum_enabled = System.get_env("RUM_ENABLED") == "true"
config :pleroma, :database, rum_enabled: rum_enabled
IO.puts("RUM enabled: #{rum_enabled}")
config :joken, default_signer: "yU8uHKq+yyAkZ11Hx//jcdacWc8yQ1bxAAGrplzB0Zwwjkp35v0RK9SO8WTPr6QZ"
config :pleroma, Pleroma.ReverseProxy.Client, Pleroma.ReverseProxy.ClientMock
config :pleroma, :modules, runtime_dir: "test/fixtures/modules"
config :pleroma, Pleroma.Gun, Pleroma.GunMock
config :pleroma, Pleroma.Emails.NewUsersDigestEmail, enabled: true
config :pleroma, Pleroma.Web.Plugs.RemoteIp, enabled: false
config :pleroma, Pleroma.Web.ApiSpec.CastAndValidate, strict: true
config :tzdata, :autoupdate, :disabled
config :pleroma, :mrf, policies: []
config :pleroma, :pipeline,
object_validator: Pleroma.Web.ActivityPub.ObjectValidatorMock,
mrf: Pleroma.Web.ActivityPub.MRFMock,
activity_pub: Pleroma.Web.ActivityPub.ActivityPubMock,
side_effects: Pleroma.Web.ActivityPub.SideEffectsMock,
federator: Pleroma.Web.FederatorMock,
config: Pleroma.ConfigMock
config :pleroma, :cachex, provider: Pleroma.CachexMock
config :pleroma, Pleroma.Web.WebFinger, update_nickname_on_user_fetch: false
config :pleroma, :side_effects,
ap_streamer: Pleroma.Web.ActivityPub.ActivityPubMock,
logger: Pleroma.LoggerMock
config :pleroma, Pleroma.Search, module: Pleroma.Search.DatabaseSearch
config :pleroma, Pleroma.Search.Meilisearch, url: "http://127.0.0.1:7700/", private_key: nil
# Reduce recompilation time
# https://dashbit.co/blog/speeding-up-re-compilation-of-elixir-projects
config :phoenix, :plug_init_mode, :runtime
config :pleroma, :config_impl, Pleroma.UnstubbedConfigMock
config :pleroma, Pleroma.PromEx, disabled: true
# Mox definitions. Only read during compile time.
config :pleroma, Pleroma.User.Backup, config_impl: Pleroma.UnstubbedConfigMock
config :pleroma, Pleroma.Uploaders.S3, ex_aws_impl: Pleroma.Uploaders.S3.ExAwsMock
config :pleroma, Pleroma.Uploaders.S3, config_impl: Pleroma.UnstubbedConfigMock
config :pleroma, Pleroma.Upload, config_impl: Pleroma.UnstubbedConfigMock
config :pleroma, Pleroma.ScheduledActivity, config_impl: Pleroma.UnstubbedConfigMock
config :pleroma, Pleroma.Web.RichMedia.Helpers, config_impl: Pleroma.StaticStubbedConfigMock
peer_module =
if String.to_integer(System.otp_release()) >= 25 do
:peer
else
:slave
end
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
IO.puts(
"You may want to create test.secret.exs to declare custom database connection parameters."
)
end
diff --git a/lib/pleroma/html.ex b/lib/pleroma/html.ex
index 84ff2f129..4de7cbb76 100644
--- a/lib/pleroma/html.ex
+++ b/lib/pleroma/html.ex
@@ -1,84 +1,80 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.HTML do
# Scrubbers are compiled on boot so they can be configured in OTP releases
# @on_load :compile_scrubbers
def compile_scrubbers do
dir = Path.join(:code.priv_dir(:pleroma), "scrubbers")
dir
|> Pleroma.Utils.compile_dir()
|> case do
{:error, _errors, _warnings} ->
raise "Compiling scrubbers failed"
{:ok, _modules, _warnings} ->
:ok
end
end
defp get_scrubbers(scrubber) when is_atom(scrubber), do: [scrubber]
defp get_scrubbers(scrubbers) when is_list(scrubbers), do: scrubbers
defp get_scrubbers(_), do: [Pleroma.HTML.Scrubber.Default]
def get_scrubbers do
Pleroma.Config.get([:markup, :scrub_policy])
|> get_scrubbers
end
def filter_tags(html, nil) do
filter_tags(html, get_scrubbers())
end
def filter_tags(html, scrubbers) when is_list(scrubbers) do
Enum.reduce(scrubbers, html, fn scrubber, html ->
filter_tags(html, scrubber)
end)
end
def filter_tags(html, scrubber) do
{:ok, content} = FastSanitize.Sanitizer.scrub(html, scrubber)
content
end
def filter_tags(html), do: filter_tags(html, nil)
def strip_tags(html), do: filter_tags(html, FastSanitize.Sanitizer.StripTags)
def ensure_scrubbed_html(
content,
scrubbers,
fake,
callback
) do
content =
content
|> filter_tags(scrubbers)
|> callback.()
if fake do
{:ignore, content}
else
{:commit, content}
end
end
- @spec extract_first_external_url_from_object(Pleroma.Object.t()) ::
- {:ok, String.t()} | {:error, :no_content}
+ @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
- url =
- content
- |> Floki.parse_fragment!()
- |> Floki.find("a:not(.mention,.hashtag,.attachment,[rel~=\"tag\"])")
- |> Enum.take(1)
- |> Floki.attribute("href")
- |> Enum.at(0)
-
- {:ok, url}
+ content
+ |> Floki.parse_fragment!()
+ |> Floki.find("a:not(.mention,.hashtag,.attachment,[rel~=\"tag\"])")
+ |> Enum.take(1)
+ |> Floki.attribute("href")
+ |> Enum.at(0)
end
- def extract_first_external_url_from_object(_), do: {:error, :no_content}
+ def extract_first_external_url_from_object(_), do: nil
end
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index 2017c696d..a1fccc705 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -1,1851 +1,1849 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ActivityPub do
alias Pleroma.Activity
alias Pleroma.Activity.Ir.Topics
alias Pleroma.Config
alias Pleroma.Constants
alias Pleroma.Conversation
alias Pleroma.Conversation.Participation
alias Pleroma.Filter
alias Pleroma.Hashtag
alias Pleroma.Maps
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Object.Containment
alias Pleroma.Object.Fetcher
alias Pleroma.Pagination
alias Pleroma.Repo
alias Pleroma.Upload
alias Pleroma.User
alias Pleroma.Web.ActivityPub.MRF
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.Streamer
alias Pleroma.Web.WebFinger
alias Pleroma.Workers.BackgroundWorker
alias Pleroma.Workers.PollWorker
import Ecto.Query
import Pleroma.Web.ActivityPub.Utils
import Pleroma.Web.ActivityPub.Visibility
require Logger
require Pleroma.Constants
@behaviour Pleroma.Web.ActivityPub.ActivityPub.Persisting
@behaviour Pleroma.Web.ActivityPub.ActivityPub.Streaming
defp get_recipients(%{"type" => "Create"} = data) do
to = Map.get(data, "to", [])
cc = Map.get(data, "cc", [])
bcc = Map.get(data, "bcc", [])
actor = Map.get(data, "actor", [])
recipients = [to, cc, bcc, [actor]] |> Enum.concat() |> Enum.uniq()
{recipients, to, cc}
end
defp get_recipients(data) do
to = Map.get(data, "to", [])
cc = Map.get(data, "cc", [])
bcc = Map.get(data, "bcc", [])
recipients = Enum.concat([to, cc, bcc])
{recipients, to, cc}
end
defp check_actor_can_insert(%{"type" => "Delete"}), do: true
defp check_actor_can_insert(%{"type" => "Undo"}), do: true
defp check_actor_can_insert(%{"actor" => actor}) when is_binary(actor) do
case User.get_cached_by_ap_id(actor) do
%User{is_active: true} -> true
_ -> false
end
end
defp check_actor_can_insert(_), do: true
defp check_remote_limit(%{"object" => %{"content" => content}}) when not is_nil(content) do
limit = Config.get([:instance, :remote_limit])
String.length(content) <= limit
end
defp check_remote_limit(_), do: true
def increase_note_count_if_public(actor, object) do
if public?(object), do: User.increase_note_count(actor), else: {:ok, actor}
end
def decrease_note_count_if_public(actor, object) do
if public?(object), do: User.decrease_note_count(actor), else: {:ok, actor}
end
def update_last_status_at_if_public(actor, object) do
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 public?(object) do
Object.increase_replies_count(reply_ap_id)
end
end
defp increase_replies_count_if_reply(_create_data), do: :noop
defp increase_quotes_count_if_quote(%{
"object" => %{"quoteUrl" => quote_ap_id} = object,
"type" => "Create"
}) do
if public?(object) do
Object.increase_quotes_count(quote_ap_id)
end
end
defp increase_quotes_count_if_quote(_create_data), do: :noop
@object_types ~w[ChatMessage Question Answer Audio Video Image Event Article Note Page]
@impl true
def persist(%{"type" => type} = object, meta) when type in @object_types do
with {:ok, object} <- Object.create(object) do
{:ok, object, meta}
end
end
@impl true
def persist(object, meta) do
with local <- Keyword.fetch!(meta, :local),
{recipients, _, _} <- get_recipients(object),
{:ok, activity} <-
Repo.insert(%Activity{
data: object,
local: local,
recipients: recipients,
actor: object["actor"]
}),
# TODO: add tests for expired activities, when Note type will be supported in new pipeline
{:ok, _} <- maybe_create_activity_expiration(activity) do
{:ok, activity, meta}
end
end
@spec insert(map(), boolean(), boolean(), boolean()) :: {:ok, Activity.t()} | {:error, any()}
def insert(map, local \\ true, fake \\ false, bypass_actor_check \\ false) when is_map(map) do
with nil <- Activity.normalize(map),
map <- lazy_put_activity_defaults(map, fake),
{_, true} <- {:actor_check, bypass_actor_check || check_actor_can_insert(map)},
{_, true} <- {:remote_limit_pass, check_remote_limit(map)},
{:ok, map} <- MRF.filter(map),
{recipients, _, _} = get_recipients(map),
{:fake, false, map, recipients} <- {:fake, fake, map, recipients},
{:containment, :ok} <- {:containment, Containment.contain_child(map)},
{:ok, map, object} <- insert_full_object(map),
{:ok, activity} <- insert_activity_with_expiration(map, local, recipients) 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)
{:ok, activity}
else
%Activity{} = activity ->
{:ok, activity}
{:actor_check, _} ->
{:error, false}
{:containment, _} = error ->
error
{:error, _} = error ->
error
{:fake, true, map, recipients} ->
activity = %Activity{
data: map,
local: local,
actor: map["actor"],
recipients: recipients,
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, _} ->
{:error, :remote_limit}
{:reject, _} = e ->
{:error, e}
end
end
defp insert_activity_with_expiration(data, local, recipients) do
struct = %Activity{
data: data,
local: local,
actor: data["actor"],
recipients: recipients
}
with {:ok, activity} <- Repo.insert(struct) do
maybe_create_activity_expiration(activity)
end
end
def notify_and_stream(activity) do
Notification.create_notifications(activity)
original_activity =
case activity do
%{data: %{"type" => "Update"}, object: %{data: %{"id" => id}}} ->
Activity.get_create_by_object_ap_id_with_object(id)
_ ->
activity
end
conversation = create_or_bump_conversation(original_activity, original_activity.actor)
participations = get_participations(conversation)
stream_out(activity)
stream_out_participations(participations)
end
defp maybe_create_activity_expiration(
%{data: %{"expires_at" => %DateTime{} = expires_at}} = activity
) do
with {:ok, _job} <-
Pleroma.Workers.PurgeExpiredActivity.enqueue(%{
activity_id: activity.id,
expires_at: expires_at
}) do
{:ok, activity}
end
end
defp maybe_create_activity_expiration(activity), do: {:ok, activity}
defp create_or_bump_conversation(activity, actor) do
with {:ok, conversation} <- Conversation.create_or_bump_for(activity),
%User{} = user <- User.get_cached_by_ap_id(actor) do
Participation.mark_as_read(user, conversation)
{:ok, conversation}
end
end
defp get_participations({:ok, conversation}) do
conversation
|> Repo.preload(:participations, force: true)
|> Map.get(:participations)
end
defp get_participations(_), do: []
def stream_out_participations(participations) do
participations =
participations
|> Repo.preload(:user)
Streamer.stream("participation", participations)
end
@impl true
def stream_out_participations(%Object{data: %{"context" => context}}, user) do
with %Conversation{} = conversation <- Conversation.get_for_ap_id(context) do
conversation = Repo.preload(conversation, :participations)
last_activity_id =
fetch_latest_direct_activity_id_for_context(conversation.ap_id, %{
user: user,
blocking_user: user
})
if last_activity_id do
stream_out_participations(conversation.participations)
end
end
end
@impl true
def stream_out_participations(_, _), do: :noop
@impl true
def stream_out(%Activity{data: %{"type" => data_type}} = activity)
when data_type in ["Create", "Announce", "Delete", "Update"] do
activity
|> Topics.get_activity_topics()
|> Streamer.stream(activity)
end
@impl true
def stream_out(_activity) do
:noop
end
@spec create(map(), boolean()) :: {:ok, Activity.t()} | {:error, any()}
def create(params, fake \\ false) do
with {:ok, result} <- Repo.transaction(fn -> do_create(params, fake) end) do
result
end
end
defp do_create(%{to: to, actor: actor, context: context, object: object} = params, fake) do
additional = params[:additional] || %{}
# only accept false as false value
local = !(params[:local] == false)
published = params[:published]
quick_insert? = Config.get([:env]) == :benchmark
create_data =
make_create_data(
%{to: to, actor: actor, published: published, context: context, object: object},
additional
)
with {:ok, activity} <- insert(create_data, local, fake),
{:fake, false, activity} <- {:fake, fake, activity},
_ <- increase_replies_count_if_reply(create_data),
_ <- increase_quotes_count_if_quote(create_data),
{:quick_insert, false, activity} <- {:quick_insert, quick_insert?, activity},
{:ok, _actor} <- increase_note_count_if_public(actor, activity),
{: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
{:quick_insert, true, activity} ->
{:ok, activity}
{:fake, true, activity} ->
{:ok, activity}
{:error, message} ->
Repo.rollback(message)
end
end
defp maybe_schedule_poll_notifications(activity) do
PollWorker.schedule_poll_end(activity)
:ok
end
@spec listen(map()) :: {:ok, Activity.t()} | {:error, any()}
def listen(%{to: to, actor: actor, context: context, object: object} = params) do
additional = params[:additional] || %{}
# only accept false as false value
local = !(params[:local] == false)
published = params[:published]
listen_data =
make_listen_data(
%{to: to, actor: actor, published: published, context: context, object: object},
additional
)
with {:ok, activity} <- insert(listen_data, local),
_ <- notify_and_stream(activity),
:ok <- maybe_federate(activity) do
{:ok, activity}
end
end
@spec unfollow(User.t(), User.t(), String.t() | nil, boolean()) ::
{:ok, Activity.t()} | nil | {:error, any()}
def unfollow(follower, followed, activity_id \\ nil, local \\ true) do
with {:ok, result} <-
Repo.transaction(fn -> do_unfollow(follower, followed, activity_id, local) end) do
result
end
end
defp do_unfollow(follower, followed, activity_id, local) do
with %Activity{} = follow_activity <- fetch_latest_follow(follower, followed),
{:ok, follow_activity} <- update_follow_state(follow_activity, "cancelled"),
unfollow_data <- make_unfollow_data(follower, followed, follow_activity, activity_id),
{:ok, activity} <- insert(unfollow_data, local),
_ <- notify_and_stream(activity),
:ok <- maybe_federate(activity) do
{:ok, activity}
else
nil -> nil
{:error, error} -> Repo.rollback(error)
end
end
@spec flag(map()) :: {:ok, Activity.t()} | {:error, any()}
def flag(params) do
with {:ok, result} <- Repo.transaction(fn -> do_flag(params) end) do
result
end
end
defp do_flag(
%{
actor: actor,
context: _context,
account: account,
statuses: statuses,
content: content
} = params
) do
# only accept false as false value
local = !(params[:local] == false)
forward = !(params[:forward] == false)
additional = params[:additional] || %{}
additional =
if forward do
Map.merge(additional, %{"to" => [], "cc" => [account.ap_id]})
else
Map.merge(additional, %{"to" => [], "cc" => []})
end
with flag_data <- make_flag_data(params, additional),
{:ok, activity} <- insert(flag_data, local),
{:ok, stripped_activity} <- strip_report_status_data(activity),
_ <- notify_and_stream(activity),
:ok <-
maybe_federate(stripped_activity) do
User.all_users_with_privilege(:reports_manage_reports)
|> Enum.filter(fn user -> user.ap_id != actor end)
|> Enum.filter(fn user -> not is_nil(user.email) end)
|> Enum.each(fn privileged_user ->
privileged_user
|> Pleroma.Emails.AdminEmail.report(actor, account, statuses, content)
|> Pleroma.Emails.Mailer.deliver_async()
end)
{:ok, activity}
else
{:error, error} -> Repo.rollback(error)
end
end
@spec move(User.t(), User.t(), boolean()) :: {:ok, Activity.t()} | {:error, any()}
def move(%User{} = origin, %User{} = target, local \\ true) do
params = %{
"type" => "Move",
"actor" => origin.ap_id,
"object" => origin.ap_id,
"target" => target.ap_id,
"to" => [origin.follower_address]
}
with true <- origin.ap_id in target.also_known_as,
{:ok, activity} <- insert(params, local),
_ <- notify_and_stream(activity) do
maybe_federate(activity)
BackgroundWorker.enqueue("move_following", %{
"origin_id" => origin.id,
"target_id" => target.id
})
{:ok, activity}
else
false -> {:error, "Target account must have the origin in `alsoKnownAs`"}
err -> err
end
end
def fetch_activities_for_context_query(context, opts) do
public = [Constants.as_public()]
recipients =
if opts[:user],
do: [opts[:user].ap_id | User.following(opts[:user])] ++ public,
else: public
from(activity in Activity)
|> maybe_preload_objects(opts)
|> maybe_preload_bookmarks(opts)
|> maybe_set_thread_muted_field(opts)
|> restrict_unauthenticated(opts[:user])
|> restrict_blocked(opts)
|> restrict_blockers_visibility(opts)
|> restrict_recipients(recipients, opts[:user])
|> restrict_filtered(opts)
|> where(
[activity],
fragment(
"?->>'type' = ? and ?->>'context' = ?",
activity.data,
"Create",
activity.data,
^context
)
)
|> exclude_poll_votes(opts)
|> exclude_id(opts)
|> order_by([activity], desc: activity.id)
end
@spec fetch_activities_for_context(String.t(), keyword() | map()) :: [Activity.t()]
def fetch_activities_for_context(context, opts \\ %{}) do
context
|> fetch_activities_for_context_query(opts)
|> Repo.all()
end
@spec fetch_latest_direct_activity_id_for_context(String.t(), keyword() | map()) ::
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))
|> restrict_visibility(%{visibility: "direct"})
|> limit(1)
|> select([a], a.id)
|> Repo.one()
end
defp fetch_paginated_optimized(query, opts, pagination) do
# Note: tag-filtering funcs may apply "ORDER BY objects.id DESC",
# and extra sorting on "activities.id DESC NULLS LAST" would worse the query plan
opts = Map.put(opts, :skip_extra_order, true)
Pagination.fetch_paginated(query, opts, pagination)
end
def fetch_activities(recipients, opts \\ %{}, pagination \\ :keyset) do
list_memberships = Pleroma.List.memberships(opts[:user])
fetch_activities_query(recipients ++ list_memberships, opts)
|> fetch_paginated_optimized(opts, pagination)
|> Enum.reverse()
|> maybe_update_cc(list_memberships, opts[:user])
end
@spec fetch_public_or_unlisted_activities(map(), Pagination.type()) :: [Activity.t()]
def fetch_public_or_unlisted_activities(opts \\ %{}, pagination \\ :keyset) do
includes_local_public = Map.get(opts, :includes_local_public, false)
opts = Map.delete(opts, :user)
intended_recipients =
if includes_local_public do
[Constants.as_public(), as_local_public()]
else
[Constants.as_public()]
end
intended_recipients
|> fetch_activities_query(opts)
|> restrict_unlisted(opts)
|> fetch_paginated_optimized(opts, pagination)
end
@spec fetch_public_activities(map(), Pagination.type()) :: [Activity.t()]
def fetch_public_activities(opts \\ %{}, pagination \\ :keyset) do
opts
|> Map.put(:restrict_unlisted, true)
|> fetch_public_or_unlisted_activities(pagination)
end
@valid_visibilities ~w[direct unlisted public private]
defp restrict_visibility(query, %{visibility: visibility})
when is_list(visibility) do
if Enum.all?(visibility, &(&1 in @valid_visibilities)) do
from(
a in query,
where:
fragment(
"activity_visibility(?, ?, ?) = ANY (?)",
a.actor,
a.recipients,
a.data,
^visibility
)
)
else
Logger.error("Could not restrict visibility to #{visibility}")
end
end
defp restrict_visibility(query, %{visibility: visibility})
when visibility in @valid_visibilities do
from(
a in query,
where:
fragment("activity_visibility(?, ?, ?) = ?", a.actor, a.recipients, a.data, ^visibility)
)
end
defp restrict_visibility(_query, %{visibility: visibility})
when visibility not in @valid_visibilities do
Logger.error("Could not restrict visibility to #{visibility}")
end
defp restrict_visibility(query, _visibility), do: query
defp exclude_visibility(query, %{exclude_visibilities: visibility})
when is_list(visibility) do
if Enum.all?(visibility, &(&1 in @valid_visibilities)) do
from(
a in query,
where:
not fragment(
"activity_visibility(?, ?, ?) = ANY (?)",
a.actor,
a.recipients,
a.data,
^visibility
)
)
else
Logger.error("Could not exclude visibility to #{visibility}")
query
end
end
defp exclude_visibility(query, %{exclude_visibilities: visibility})
when visibility in @valid_visibilities do
from(
a in query,
where:
not fragment(
"activity_visibility(?, ?, ?) = ?",
a.actor,
a.recipients,
a.data,
^visibility
)
)
end
defp exclude_visibility(query, %{exclude_visibilities: visibility})
when visibility not in [nil | @valid_visibilities] do
Logger.error("Could not exclude visibility to #{visibility}")
query
end
defp exclude_visibility(query, _visibility), do: query
defp restrict_thread_visibility(query, _, %{skip_thread_containment: true} = _),
do: query
defp restrict_thread_visibility(query, %{user: %User{skip_thread_containment: true}}, _),
do: query
defp restrict_thread_visibility(query, %{user: %User{ap_id: ap_id}}, _) do
local_public = as_local_public()
from(
a in query,
where: fragment("thread_visibility(?, (?)->>'id', ?) = true", ^ap_id, a.data, ^local_public)
)
end
defp restrict_thread_visibility(query, _, _), do: query
def fetch_user_abstract_activities(user, reading_user, params \\ %{}) do
params =
params
|> Map.put(:user, reading_user)
|> Map.put(:actor_id, user.ap_id)
%{
godmode: params[:godmode],
reading_user: reading_user
}
|> user_activities_recipients()
|> fetch_activities(params)
|> Enum.reverse()
end
def fetch_user_activities(user, reading_user, params \\ %{})
def fetch_user_activities(user, reading_user, %{total: true} = params) do
result = fetch_activities_for_user(user, reading_user, params)
Keyword.put(result, :items, Enum.reverse(result[:items]))
end
def fetch_user_activities(user, reading_user, params) do
user
|> fetch_activities_for_user(reading_user, params)
|> Enum.reverse()
end
defp fetch_activities_for_user(user, reading_user, params) do
params =
params
|> Map.put(:type, ["Create", "Announce"])
|> Map.put(:user, reading_user)
|> Map.put(:actor_id, user.ap_id)
|> Map.put(:pinned_object_ids, Map.keys(user.pinned_objects))
params =
if User.blocks?(reading_user, user) do
params
else
params
|> Map.put(:blocking_user, reading_user)
|> Map.put(:muting_user, reading_user)
end
pagination_type = Map.get(params, :pagination_type) || :keyset
%{
godmode: params[:godmode],
reading_user: reading_user
}
|> user_activities_recipients()
|> fetch_activities(params, pagination_type)
end
def fetch_statuses(reading_user, %{total: true} = params) do
result = fetch_activities_for_reading_user(reading_user, params)
Keyword.put(result, :items, Enum.reverse(result[:items]))
end
def fetch_statuses(reading_user, params) do
reading_user
|> fetch_activities_for_reading_user(params)
|> Enum.reverse()
end
defp fetch_activities_for_reading_user(reading_user, params) do
params = Map.put(params, :type, ["Create", "Announce"])
%{
godmode: params[:godmode],
reading_user: reading_user
}
|> user_activities_recipients()
|> fetch_activities(params, :offset)
end
defp user_activities_recipients(%{godmode: true}), do: []
defp user_activities_recipients(%{reading_user: reading_user}) do
if not is_nil(reading_user) and reading_user.local do
[
Constants.as_public(),
as_local_public(),
reading_user.ap_id | User.following(reading_user)
]
else
[Constants.as_public()]
end
end
defp restrict_announce_object_actor(_query, %{announce_filtering_user: _, skip_preload: true}) do
raise "Can't use the child object without preloading!"
end
defp restrict_announce_object_actor(query, %{announce_filtering_user: %{ap_id: actor}}) do
from(
[activity, object] in query,
where:
fragment(
"?->>'type' != ? or ?->>'actor' != ?",
activity.data,
"Announce",
object.data,
^actor
)
)
end
defp restrict_announce_object_actor(query, _), do: query
defp restrict_since(query, %{since_id: ""}), do: query
defp restrict_since(query, %{since_id: since_id}) do
from(activity in query, where: activity.id > ^since_id)
end
defp restrict_since(query, _), do: query
defp restrict_embedded_tag_all(_query, %{tag_all: _tag_all, skip_preload: true}) do
raise_on_missing_preload()
end
defp restrict_embedded_tag_all(query, %{tag_all: [_ | _] = tag_all}) do
from(
[_activity, object] in query,
where: fragment("(?)->'tag' \\?& (?)", object.data, ^tag_all)
)
end
defp restrict_embedded_tag_all(query, %{tag_all: tag}) when is_binary(tag) do
restrict_embedded_tag_any(query, %{tag: tag})
end
defp restrict_embedded_tag_all(query, _), do: query
defp restrict_embedded_tag_any(_query, %{tag: _tag, skip_preload: true}) do
raise_on_missing_preload()
end
defp restrict_embedded_tag_any(query, %{tag: [_ | _] = tag_any}) do
from(
[_activity, object] in query,
where: fragment("(?)->'tag' \\?| (?)", object.data, ^tag_any)
)
end
defp restrict_embedded_tag_any(query, %{tag: tag}) when is_binary(tag) do
restrict_embedded_tag_any(query, %{tag: [tag]})
end
defp restrict_embedded_tag_any(query, _), do: query
defp restrict_embedded_tag_reject_any(_query, %{tag_reject: _tag_reject, skip_preload: true}) do
raise_on_missing_preload()
end
defp restrict_embedded_tag_reject_any(query, %{tag_reject: [_ | _] = tag_reject}) do
from(
[_activity, object] in query,
where: fragment("not (?)->'tag' \\?| (?)", object.data, ^tag_reject)
)
end
defp restrict_embedded_tag_reject_any(query, %{tag_reject: tag_reject})
when is_binary(tag_reject) do
restrict_embedded_tag_reject_any(query, %{tag_reject: [tag_reject]})
end
defp restrict_embedded_tag_reject_any(query, _), do: query
defp object_ids_query_for_tags(tags) do
from(hto in "hashtags_objects")
|> join(:inner, [hto], ht in Pleroma.Hashtag, on: hto.hashtag_id == ht.id)
|> where([hto, ht], ht.name in ^tags)
|> select([hto], hto.object_id)
|> distinct([hto], true)
end
defp restrict_hashtag_all(_query, %{tag_all: _tag, skip_preload: true}) do
raise_on_missing_preload()
end
defp restrict_hashtag_all(query, %{tag_all: [single_tag]}) do
restrict_hashtag_any(query, %{tag: single_tag})
end
defp restrict_hashtag_all(query, %{tag_all: [_ | _] = tags}) do
from(
[_activity, object] in query,
where:
fragment(
"""
(SELECT array_agg(hashtags.name) FROM hashtags JOIN hashtags_objects
ON hashtags_objects.hashtag_id = hashtags.id WHERE hashtags.name = ANY(?)
AND hashtags_objects.object_id = ?) @> ?
""",
^tags,
object.id,
^tags
)
)
end
defp restrict_hashtag_all(query, %{tag_all: tag}) when is_binary(tag) do
restrict_hashtag_all(query, %{tag_all: [tag]})
end
defp restrict_hashtag_all(query, _), do: query
defp restrict_hashtag_any(_query, %{tag: _tag, skip_preload: true}) do
raise_on_missing_preload()
end
defp restrict_hashtag_any(query, %{tag: [_ | _] = tags}) do
hashtag_ids =
from(ht in Hashtag, where: ht.name in ^tags, select: ht.id)
|> Repo.all()
# Note: NO extra ordering should be done on "activities.id desc nulls last" for optimal plan
from(
[_activity, object] in query,
join: hto in "hashtags_objects",
on: hto.object_id == object.id,
where: hto.hashtag_id in ^hashtag_ids,
distinct: [desc: object.id],
order_by: [desc: object.id]
)
end
defp restrict_hashtag_any(query, %{tag: tag}) when is_binary(tag) do
restrict_hashtag_any(query, %{tag: [tag]})
end
defp restrict_hashtag_any(query, _), do: query
defp restrict_hashtag_reject_any(_query, %{tag_reject: _tag_reject, skip_preload: true}) do
raise_on_missing_preload()
end
defp restrict_hashtag_reject_any(query, %{tag_reject: [_ | _] = tags_reject}) do
from(
[_activity, object] in query,
where: object.id not in subquery(object_ids_query_for_tags(tags_reject))
)
end
defp restrict_hashtag_reject_any(query, %{tag_reject: tag_reject}) when is_binary(tag_reject) do
restrict_hashtag_reject_any(query, %{tag_reject: [tag_reject]})
end
defp restrict_hashtag_reject_any(query, _), do: query
defp raise_on_missing_preload do
raise "Can't use the child object without preloading!"
end
defp restrict_recipients(query, [], _user), do: query
defp restrict_recipients(query, recipients, nil) do
from(activity in query, where: fragment("? && ?", ^recipients, activity.recipients))
end
defp restrict_recipients(query, recipients, user) do
from(
activity in query,
where: fragment("? && ?", ^recipients, activity.recipients),
or_where: activity.actor == ^user.ap_id
)
end
defp restrict_local(query, %{local_only: true}) do
from(activity in query, where: activity.local == true)
end
defp restrict_local(query, _), do: query
defp restrict_remote(query, %{remote: true}) do
from(activity in query, where: activity.local == false)
end
defp restrict_remote(query, _), do: query
defp restrict_actor(query, %{actor_id: actor_id}) do
from(activity in query, where: activity.actor == ^actor_id)
end
defp restrict_actor(query, _), do: query
defp restrict_type(query, %{type: type}) when is_binary(type) do
from(activity in query, where: fragment("?->>'type' = ?", activity.data, ^type))
end
defp restrict_type(query, %{type: type}) do
from(activity in query, where: fragment("?->>'type' = ANY(?)", activity.data, ^type))
end
defp restrict_type(query, _), do: query
defp restrict_state(query, %{state: state}) do
from(activity in query, where: fragment("?->>'state' = ?", activity.data, ^state))
end
defp restrict_state(query, _), do: query
defp restrict_favorited_by(query, %{favorited_by: ap_id}) do
from(
[_activity, object] in query,
where: fragment("(?)->'likes' \\? (?)", object.data, ^ap_id)
)
end
defp restrict_favorited_by(query, _), do: query
defp restrict_media(_query, %{only_media: _val, skip_preload: true}) do
raise "Can't use the child object without preloading!"
end
defp restrict_media(query, %{only_media: true}) do
from(
[activity, object] in query,
where: fragment("(?)->>'type' = ?", activity.data, "Create"),
where: fragment("not (?)->'attachment' = (?)", object.data, ^[])
)
end
defp restrict_media(query, _), do: query
defp restrict_replies(query, %{exclude_replies: true}) do
from(
[_activity, object] in query,
where: fragment("?->>'inReplyTo' is null", object.data)
)
end
defp restrict_replies(query, %{
reply_filtering_user: %User{} = user,
reply_visibility: "self"
}) do
from(
[activity, object] in query,
where:
fragment(
"?->>'inReplyTo' is null OR ? = ANY(?)",
object.data,
^user.ap_id,
activity.recipients
)
)
end
defp restrict_replies(query, %{
reply_filtering_user: %User{} = user,
reply_visibility: "following"
}) do
from(
[activity, object] in query,
where:
fragment(
"""
?->>'type' != 'Create' -- This isn't a Create
OR ?->>'inReplyTo' is null -- this isn't a reply
OR ? && array_remove(?, ?) -- The recipient is us or one of our friends,
-- unless they are the author (because authors
-- are also part of the recipients). This leads
-- to a bug that self-replies by friends won't
-- show up.
OR ? = ? -- The actor is us
""",
activity.data,
object.data,
^[user.ap_id | User.get_cached_user_friends_ap_ids(user)],
activity.recipients,
activity.actor,
activity.actor,
^user.ap_id
)
)
end
defp restrict_replies(query, _), do: query
defp restrict_reblogs(query, %{exclude_reblogs: true}) do
from(activity in query, where: fragment("?->>'type' != 'Announce'", activity.data))
end
defp restrict_reblogs(query, _), do: query
defp restrict_muted(query, %{with_muted: true}), do: query
defp restrict_muted(query, %{muting_user: %User{} = user} = opts) do
mutes = opts[:muted_users_ap_ids] || User.muted_users_ap_ids(user)
query =
from([activity] in query,
where: fragment("not (? = ANY(?))", activity.actor, ^mutes),
where:
fragment(
"not (?->'to' \\?| ?) or ? = ?",
activity.data,
^mutes,
activity.actor,
^user.ap_id
)
)
unless opts[:skip_preload] do
from([thread_mute: tm] in query, where: is_nil(tm.user_id))
else
query
end
end
defp restrict_muted(query, _), do: query
defp restrict_blocked(query, %{blocking_user: %User{} = user} = opts) do
blocked_ap_ids = opts[:blocked_users_ap_ids] || User.blocked_users_ap_ids(user)
domain_blocks = user.domain_blocks || []
following_ap_ids = User.get_friends_ap_ids(user)
query =
if has_named_binding?(query, :object), do: query, else: Activity.with_joined_object(query)
from(
[activity, object: o] in query,
# You don't block the author
where: fragment("not (? = ANY(?))", activity.actor, ^blocked_ap_ids),
# You don't block any recipients, and didn't author the post
where:
fragment(
"((not (? && ?)) or ? = ?)",
activity.recipients,
^blocked_ap_ids,
activity.actor,
^user.ap_id
),
# You don't block the domain of any recipients, and didn't author the post
where:
fragment(
"(recipients_contain_blocked_domains(?, ?) = false) or ? = ?",
activity.recipients,
^domain_blocks,
activity.actor,
^user.ap_id
),
# It's not a boost of a user you block
where:
fragment(
"not (?->>'type' = 'Announce' and ?->'to' \\?| ?)",
activity.data,
activity.data,
^blocked_ap_ids
),
# You don't block the author's domain, and also don't follow the author
where:
fragment(
"(not (split_part(?, '/', 3) = ANY(?))) or ? = ANY(?)",
activity.actor,
^domain_blocks,
activity.actor,
^following_ap_ids
),
# Same as above, but checks the Object
where:
fragment(
"(not (split_part(?->>'actor', '/', 3) = ANY(?))) or (?->>'actor') = ANY(?)",
o.data,
^domain_blocks,
o.data,
^following_ap_ids
)
)
end
defp restrict_blocked(query, _), do: query
defp restrict_blockers_visibility(query, %{blocking_user: %User{} = user}) do
if Config.get([:activitypub, :blockers_visible]) == true do
query
else
blocker_ap_ids = User.incoming_relationships_ungrouped_ap_ids(user, [:block])
from(
activity in query,
# The author doesn't block you
where: fragment("not (? = ANY(?))", activity.actor, ^blocker_ap_ids),
# It's not a boost of a user that blocks you
where:
fragment(
"not (?->>'type' = 'Announce' and ?->'to' \\?| ?)",
activity.data,
activity.data,
^blocker_ap_ids
)
)
end
end
defp restrict_blockers_visibility(query, _), do: query
defp restrict_unlisted(query, %{restrict_unlisted: true}) do
from(
activity in query,
where:
fragment(
"not (coalesce(?->'cc', '{}'::jsonb) \\?| ?)",
activity.data,
^[Constants.as_public()]
)
)
end
defp restrict_unlisted(query, _), do: query
defp restrict_pinned(query, %{pinned: true, pinned_object_ids: ids}) do
from(
[activity, object: o] in query,
where:
fragment(
"(?)->>'type' = 'Create' and associated_object_id((?)) = any (?)",
activity.data,
activity.data,
^ids
)
)
end
defp restrict_pinned(query, _), do: query
defp restrict_muted_reblogs(query, %{muting_user: %User{} = user} = opts) do
muted_reblogs = opts[:reblog_muted_users_ap_ids] || User.reblog_muted_users_ap_ids(user)
from(
activity in query,
where:
fragment(
"not ( ?->>'type' = 'Announce' and ? = ANY(?))",
activity.data,
activity.actor,
^muted_reblogs
)
)
end
defp restrict_muted_reblogs(query, _), do: query
defp restrict_instance(query, %{instance: instance}) when is_binary(instance) do
from(
activity in query,
where: fragment("split_part(actor::text, '/'::text, 3) = ?", ^instance)
)
end
defp restrict_instance(query, _), do: query
defp restrict_filtered(query, %{user: %User{} = user}) do
case Filter.compose_regex(user) do
nil ->
query
regex ->
from([activity, object] in query,
where:
fragment("not(?->>'content' ~* ?)", object.data, ^regex) or
activity.actor == ^user.ap_id
)
end
end
defp restrict_filtered(query, %{blocking_user: %User{} = user}) do
restrict_filtered(query, %{user: user})
end
defp restrict_filtered(query, _), do: query
defp restrict_unauthenticated(query, nil) do
local = Config.restrict_unauthenticated_access?(:activities, :local)
remote = Config.restrict_unauthenticated_access?(:activities, :remote)
cond do
local and remote ->
from(activity in query, where: false)
local ->
from(activity in query, where: activity.local == false)
remote ->
from(activity in query, where: activity.local == true)
true ->
query
end
end
defp restrict_unauthenticated(query, _), do: query
defp restrict_quote_url(query, %{quote_url: quote_url}) do
from([_activity, object] in query,
where: fragment("(?)->'quoteUrl' = ?", object.data, ^quote_url)
)
end
defp restrict_quote_url(query, _), do: query
defp exclude_poll_votes(query, %{include_poll_votes: true}), do: query
defp exclude_poll_votes(query, _) do
if has_named_binding?(query, :object) do
from([activity, object: o] in query,
where: fragment("not(?->>'type' = ?)", o.data, "Answer")
)
else
query
end
end
defp exclude_chat_messages(query, %{include_chat_messages: true}), do: query
defp exclude_chat_messages(query, _) do
if has_named_binding?(query, :object) do
from([activity, object: o] in query,
where: fragment("not(?->>'type' = ?)", o.data, "ChatMessage")
)
else
query
end
end
defp exclude_invisible_actors(query, %{type: "Flag"}), do: query
defp exclude_invisible_actors(query, %{invisible_actors: true}), do: query
defp exclude_invisible_actors(query, _opts) do
query
|> join(:inner, [activity], u in User,
as: :u,
on: activity.actor == u.ap_id and u.invisible == false
)
end
defp exclude_id(query, %{exclude_id: id}) when is_binary(id) do
from(activity in query, where: activity.id != ^id)
end
defp exclude_id(query, _), do: query
defp maybe_preload_objects(query, %{skip_preload: true}), do: query
defp maybe_preload_objects(query, _) do
query
|> Activity.with_preloaded_object()
end
defp maybe_preload_bookmarks(query, %{skip_preload: true}), do: query
defp maybe_preload_bookmarks(query, opts) do
query
|> Activity.with_preloaded_bookmark(opts[:user])
end
defp maybe_preload_report_notes(query, %{preload_report_notes: true}) do
query
|> Activity.with_preloaded_report_notes()
end
defp maybe_preload_report_notes(query, _), do: query
defp maybe_set_thread_muted_field(query, %{skip_preload: true}), do: query
defp maybe_set_thread_muted_field(query, opts) do
query
|> Activity.with_set_thread_muted_field(opts[:muting_user] || opts[:user])
end
defp maybe_order(query, %{order: :desc}) do
query
|> order_by(desc: :id)
end
defp maybe_order(query, %{order: :asc}) do
query
|> order_by(asc: :id)
end
defp maybe_order(query, _), do: query
defp normalize_fetch_activities_query_opts(opts) do
Enum.reduce([:tag, :tag_all, :tag_reject], opts, fn key, opts ->
case opts[key] do
value when is_bitstring(value) ->
Map.put(opts, key, Hashtag.normalize_name(value))
value when is_list(value) ->
normalized_value =
value
|> Enum.map(&Hashtag.normalize_name/1)
|> Enum.uniq()
Map.put(opts, key, normalized_value)
_ ->
opts
end
end)
end
defp fetch_activities_query_ap_ids_ops(opts) do
source_user = opts[:muting_user]
ap_id_relationships = if source_user, do: [:mute, :reblog_mute], else: []
ap_id_relationships =
if opts[:blocking_user] && opts[:blocking_user] == source_user do
[:block | ap_id_relationships]
else
ap_id_relationships
end
preloaded_ap_ids = User.outgoing_relationships_ap_ids(source_user, ap_id_relationships)
restrict_blocked_opts = Map.merge(%{blocked_users_ap_ids: preloaded_ap_ids[:block]}, opts)
restrict_muted_opts = Map.merge(%{muted_users_ap_ids: preloaded_ap_ids[:mute]}, opts)
restrict_muted_reblogs_opts =
Map.merge(%{reblog_muted_users_ap_ids: preloaded_ap_ids[:reblog_mute]}, opts)
{restrict_blocked_opts, restrict_muted_opts, restrict_muted_reblogs_opts}
end
def fetch_activities_query(recipients, opts \\ %{}) do
opts = normalize_fetch_activities_query_opts(opts)
{restrict_blocked_opts, restrict_muted_opts, restrict_muted_reblogs_opts} =
fetch_activities_query_ap_ids_ops(opts)
config = %{
skip_thread_containment: Config.get([:instance, :skip_thread_containment])
}
query =
Activity
|> maybe_preload_objects(opts)
|> maybe_preload_bookmarks(opts)
|> maybe_preload_report_notes(opts)
|> maybe_set_thread_muted_field(opts)
|> maybe_order(opts)
|> restrict_recipients(recipients, opts[:user])
|> restrict_replies(opts)
|> restrict_since(opts)
|> restrict_local(opts)
|> restrict_remote(opts)
|> restrict_actor(opts)
|> restrict_type(opts)
|> restrict_state(opts)
|> restrict_favorited_by(opts)
|> restrict_blocked(restrict_blocked_opts)
|> restrict_blockers_visibility(opts)
|> restrict_muted(restrict_muted_opts)
|> restrict_filtered(opts)
|> restrict_media(opts)
|> restrict_visibility(opts)
|> restrict_thread_visibility(opts, config)
|> restrict_reblogs(opts)
|> restrict_pinned(opts)
|> restrict_muted_reblogs(restrict_muted_reblogs_opts)
|> restrict_instance(opts)
|> restrict_announce_object_actor(opts)
|> restrict_filtered(opts)
|> restrict_quote_url(opts)
|> maybe_restrict_deactivated_users(opts)
|> exclude_poll_votes(opts)
|> exclude_chat_messages(opts)
|> exclude_invisible_actors(opts)
|> exclude_visibility(opts)
if Config.feature_enabled?(:improved_hashtag_timeline) do
query
|> restrict_hashtag_any(opts)
|> restrict_hashtag_all(opts)
|> restrict_hashtag_reject_any(opts)
else
query
|> restrict_embedded_tag_any(opts)
|> restrict_embedded_tag_all(opts)
|> restrict_embedded_tag_reject_any(opts)
end
end
@doc """
Fetch favorites activities of user with order by sort adds to favorites
"""
@spec fetch_favourites(User.t(), map(), Pagination.type()) :: list(Activity.t())
def fetch_favourites(user, params \\ %{}, pagination \\ :keyset) do
user.ap_id
|> Activity.Queries.by_actor()
|> Activity.Queries.by_type("Like")
|> Activity.with_joined_object()
|> Object.with_joined_activity()
|> select([like, object, activity], %{activity | object: object, pagination_id: like.id})
|> order_by([like, _, _], desc_nulls_last: like.id)
|> Pagination.fetch_paginated(
Map.merge(params, %{skip_order: true}),
pagination
)
end
defp maybe_update_cc(activities, [_ | _] = list_memberships, %User{ap_id: user_ap_id}) do
Enum.map(activities, fn
%{data: %{"bcc" => [_ | _] = bcc}} = activity ->
if Enum.any?(bcc, &(&1 in list_memberships)) do
update_in(activity.data["cc"], &[user_ap_id | &1])
else
activity
end
activity ->
activity
end)
end
defp maybe_update_cc(activities, _, _), do: activities
defp fetch_activities_bounded_query(query, recipients, recipients_with_public) do
from(activity in query,
where:
fragment("? && ?", activity.recipients, ^recipients) or
(fragment("? && ?", activity.recipients, ^recipients_with_public) and
^Constants.as_public() in activity.recipients)
)
end
def fetch_activities_bounded(
recipients,
recipients_with_public,
opts \\ %{},
pagination \\ :keyset
) do
fetch_activities_query([], opts)
|> fetch_activities_bounded_query(recipients, recipients_with_public)
|> Pagination.fetch_paginated(opts, pagination)
|> Enum.reverse()
end
@spec upload(Upload.source(), keyword()) :: {:ok, Object.t()} | {:error, any()}
def upload(file, opts \\ []) do
with {:ok, data} <- Upload.store(sanitize_upload_file(file), opts) do
obj_data = Maps.put_if_present(data, "actor", opts[:actor])
Repo.insert(%Object{data: obj_data})
end
end
defp sanitize_upload_file(%Plug.Upload{filename: filename} = upload) when is_binary(filename) do
%Plug.Upload{
upload
| filename: Path.basename(filename)
}
end
defp sanitize_upload_file(upload), do: upload
@spec get_actor_url(any()) :: binary() | nil
defp get_actor_url(url) when is_binary(url), do: url
defp get_actor_url(%{"href" => href}) when is_binary(href), do: href
defp get_actor_url(url) when is_list(url) do
url
|> List.first()
|> get_actor_url()
end
defp get_actor_url(_url), do: nil
defp normalize_image(%{"url" => url}) do
%{
"type" => "Image",
"url" => [%{"href" => url}]
}
end
defp normalize_image(urls) when is_list(urls), do: urls |> List.first() |> normalize_image()
defp normalize_image(_), do: nil
defp object_to_user_data(data, additional) do
fields =
data
|> Map.get("attachment", [])
|> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
|> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
emojis =
data
|> Map.get("tag", [])
|> Enum.filter(fn
%{"type" => "Emoji"} -> true
_ -> false
end)
|> Map.new(fn %{"icon" => %{"url" => url}, "name" => name} ->
{String.trim(name, ":"), url}
end)
is_locked = data["manuallyApprovesFollowers"] || false
capabilities = data["capabilities"] || %{}
accepts_chat_messages = capabilities["acceptsChatMessages"]
data = Transmogrifier.maybe_fix_user_object(data)
is_discoverable = data["discoverable"] || false
invisible = data["invisible"] || false
actor_type = data["type"] || "Person"
featured_address = data["featured"]
{:ok, pinned_objects} = fetch_and_prepare_featured_from_ap_id(featured_address)
public_key =
if is_map(data["publicKey"]) && is_binary(data["publicKey"]["publicKeyPem"]) do
data["publicKey"]["publicKeyPem"]
end
shared_inbox =
if is_map(data["endpoints"]) && is_binary(data["endpoints"]["sharedInbox"]) do
data["endpoints"]["sharedInbox"]
end
birthday =
if is_binary(data["vcard:bday"]) do
case Date.from_iso8601(data["vcard:bday"]) do
{:ok, date} -> date
{:error, _} -> nil
end
end
show_birthday = !!birthday
# if WebFinger request was already done, we probably have acct, otherwise
# we request WebFinger here
nickname = additional[:nickname_from_acct] || generate_nickname(data)
%{
ap_id: data["id"],
uri: get_actor_url(data["url"]),
banner: normalize_image(data["image"]),
fields: fields,
emoji: emojis,
is_locked: is_locked,
is_discoverable: is_discoverable,
invisible: invisible,
avatar: normalize_image(data["icon"]),
name: data["name"],
follower_address: data["followers"],
following_address: data["following"],
featured_address: featured_address,
bio: data["summary"] || "",
actor_type: actor_type,
also_known_as: Map.get(data, "alsoKnownAs", []),
public_key: public_key,
inbox: data["inbox"],
shared_inbox: shared_inbox,
accepts_chat_messages: accepts_chat_messages,
birthday: birthday,
show_birthday: show_birthday,
pinned_objects: pinned_objects,
nickname: nickname
}
end
defp generate_nickname(%{"preferredUsername" => username} = data) when is_binary(username) do
generated = "#{username}@#{URI.parse(data["id"]).host}"
if Config.get([WebFinger, :update_nickname_on_user_fetch]) do
case WebFinger.finger(generated) do
{:ok, %{"subject" => "acct:" <> acct}} -> acct
_ -> generated
end
else
generated
end
end
# nickname can be nil because of virtual actors
defp generate_nickname(_), do: nil
def fetch_follow_information_for_user(user) do
with {:ok, following_data} <-
Fetcher.fetch_and_contain_remote_object_from_id(user.following_address),
{:ok, hide_follows} <- collection_private(following_data),
{:ok, followers_data} <-
Fetcher.fetch_and_contain_remote_object_from_id(user.follower_address),
{:ok, hide_followers} <- collection_private(followers_data) do
{:ok,
%{
hide_follows: hide_follows,
follower_count: normalize_counter(followers_data["totalItems"]),
following_count: normalize_counter(following_data["totalItems"]),
hide_followers: hide_followers
}}
else
{:error, _} = e -> e
e -> {:error, e}
end
end
defp normalize_counter(counter) when is_integer(counter), do: counter
defp normalize_counter(_), do: 0
def maybe_update_follow_information(user_data) do
with {:enabled, true} <- {:enabled, Config.get([:instance, :external_user_synchronization])},
{_, true} <- {:user_type_check, user_data[:type] in ["Person", "Service"]},
{_, true} <-
{:collections_available,
!!(user_data[:following_address] && user_data[:follower_address])},
{:ok, info} <-
fetch_follow_information_for_user(user_data) do
info = Map.merge(user_data[:info] || %{}, info)
user_data
|> Map.put(:info, info)
else
{:user_type_check, false} ->
user_data
{:collections_available, false} ->
user_data
{:enabled, false} ->
user_data
e ->
Logger.error(
"Follower/Following counter update for #{user_data.ap_id} failed.\n" <> inspect(e)
)
user_data
end
end
defp collection_private(%{"first" => %{"type" => type}})
when type in ["CollectionPage", "OrderedCollectionPage"],
do: {:ok, false}
defp collection_private(%{"first" => first}) do
with {:ok, %{"type" => type}} when type in ["CollectionPage", "OrderedCollectionPage"] <-
Fetcher.fetch_and_contain_remote_object_from_id(first) do
{:ok, false}
else
{:error, _} -> {:ok, true}
end
end
defp collection_private(_data), do: {:ok, true}
def user_data_from_user_object(data, additional \\ []) do
with {:ok, data} <- MRF.filter(data) do
{:ok, object_to_user_data(data, additional)}
else
e -> {:error, e}
end
end
defp fetch_and_prepare_user_from_ap_id(ap_id, additional) do
with {:ok, data} <- Fetcher.fetch_and_contain_remote_object_from_id(ap_id),
{:ok, data} <- user_data_from_user_object(data, additional) do
{:ok, maybe_update_follow_information(data)}
else
# If this has been deleted, only log a debug and not an error
{:error, "Object has been deleted" = e} ->
Logger.debug("Could not decode user at fetch #{ap_id}, #{inspect(e)}")
{:error, e}
{:error, {:reject, reason} = e} ->
Logger.info("Rejected user #{ap_id}: #{inspect(reason)}")
{:error, e}
{:error, e} ->
Logger.error("Could not decode user at fetch #{ap_id}, #{inspect(e)}")
{:error, e}
end
end
def maybe_handle_clashing_nickname(data) do
with nickname when is_binary(nickname) <- data[:nickname],
%User{} = old_user <- User.get_by_nickname(nickname),
{_, false} <- {:ap_id_comparison, data[:ap_id] == old_user.ap_id} do
Logger.info(
"Found an old user for #{nickname}, the old ap id is #{old_user.ap_id}, new one is #{data[:ap_id]}, renaming."
)
old_user
|> User.remote_user_changeset(%{nickname: "#{old_user.id}.#{old_user.nickname}"})
|> User.update_and_set_cache()
else
{:ap_id_comparison, true} ->
Logger.info(
"Found an old user for #{data[:nickname]}, but the ap id #{data[:ap_id]} is the same as the new user. Race condition? Not changing anything."
)
_ ->
nil
end
end
def pin_data_from_featured_collection(%{
"type" => type,
"orderedItems" => objects
})
when type in ["OrderedCollection", "Collection"] do
Map.new(objects, fn
%{"id" => object_ap_id} -> {object_ap_id, NaiveDateTime.utc_now()}
object_ap_id when is_binary(object_ap_id) -> {object_ap_id, NaiveDateTime.utc_now()}
end)
end
def pin_data_from_featured_collection(obj) do
Logger.error("Could not parse featured collection #{inspect(obj)}")
%{}
end
def fetch_and_prepare_featured_from_ap_id(nil) do
{:ok, %{}}
end
def fetch_and_prepare_featured_from_ap_id(ap_id) do
with {:ok, data} <- Fetcher.fetch_and_contain_remote_object_from_id(ap_id) do
{:ok, pin_data_from_featured_collection(data)}
else
e ->
Logger.error("Could not decode featured collection at fetch #{ap_id}, #{inspect(e)}")
{:ok, %{}}
end
end
def pinned_fetch_task(nil), do: nil
def pinned_fetch_task(%{pinned_objects: pins}) do
if Enum.all?(pins, fn {ap_id, _} ->
Object.get_cached_by_ap_id(ap_id) ||
match?({:ok, _object}, Fetcher.fetch_object_from_id(ap_id))
end) do
:ok
else
:error
end
end
def make_user_from_ap_id(ap_id, additional \\ []) do
user = User.get_cached_by_ap_id(ap_id)
with {:ok, data} <- fetch_and_prepare_user_from_ap_id(ap_id, additional) do
{:ok, _pid} = Task.start(fn -> pinned_fetch_task(data) end)
if user do
user
|> User.remote_user_changeset(data)
|> User.update_and_set_cache()
else
maybe_handle_clashing_nickname(data)
data
|> User.remote_user_changeset()
|> Repo.insert()
|> User.set_cache()
end
end
end
def make_user_from_nickname(nickname) do
with {:ok, %{"ap_id" => ap_id, "subject" => "acct:" <> acct}} when not is_nil(ap_id) <-
WebFinger.finger(nickname) do
make_user_from_ap_id(ap_id, nickname_from_acct: acct)
else
_e -> {:error, "No AP id in WebFinger"}
end
end
# filter out broken threads
defp contain_broken_threads(%Activity{} = activity, %User{} = user) do
entire_thread_visible_for_user?(activity, user)
end
# do post-processing on a specific activity
def contain_activity(%Activity{} = activity, %User{} = user) do
contain_broken_threads(activity, user)
end
def fetch_direct_messages_query do
Activity
|> restrict_type(%{type: "Create"})
|> restrict_visibility(%{visibility: "direct"})
|> order_by([activity], asc: activity.id)
end
defp maybe_restrict_deactivated_users(activity, %{type: "Flag"}), do: activity
defp maybe_restrict_deactivated_users(activity, _opts),
do: Activity.restrict_deactivated_users(activity)
end
diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex
index 5cb8a9700..7421b8ed8 100644
--- a/lib/pleroma/web/activity_pub/side_effects.ex
+++ b/lib/pleroma/web/activity_pub/side_effects.ex
@@ -1,627 +1,625 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.SideEffects do
@moduledoc """
This module looks at an inserted object and executes the side effects that it
implies. For example, a `Like` activity will increase the like count on the
liked object, a `Follow` activity will add the user to the follower
collection, and so on.
"""
alias Pleroma.Activity
alias Pleroma.Chat
alias Pleroma.Chat.MessageReference
alias Pleroma.FollowingRelationship
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
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
require Pleroma.Constants
require Logger
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
@logger Pleroma.Config.get([:side_effects, :logger], Logger)
@behaviour Pleroma.Web.ActivityPub.SideEffects.Handling
defp ap_streamer, do: Pleroma.Config.get([:side_effects, :ap_streamer], ActivityPub)
@impl true
def handle(object, meta \\ [])
# Task this handles
# - Follows
# - Sends a notification
@impl true
def handle(
%{
data: %{
"actor" => actor,
"type" => "Accept",
"object" => follow_activity_id
}
} = object,
meta
) do
with %Activity{actor: follower_id} = follow_activity <-
Activity.get_by_ap_id(follow_activity_id),
%User{} = followed <- User.get_cached_by_ap_id(actor),
%User{} = follower <- User.get_cached_by_ap_id(follower_id),
{:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
{:ok, _follower, followed} <-
FollowingRelationship.update(follower, followed, :follow_accept) do
Notification.update_notification_type(followed, follow_activity)
end
{:ok, object, meta}
end
# Task this handles
# - Rejects all existing follow activities for this person
# - Updates the follow state
# - Dismisses notification
@impl true
def handle(
%{
data: %{
"actor" => actor,
"type" => "Reject",
"object" => follow_activity_id
}
} = object,
meta
) do
with %Activity{actor: follower_id} = follow_activity <-
Activity.get_by_ap_id(follow_activity_id),
%User{} = followed <- User.get_cached_by_ap_id(actor),
%User{} = follower <- User.get_cached_by_ap_id(follower_id),
{:ok, _follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject") do
FollowingRelationship.update(follower, followed, :follow_reject)
Notification.dismiss(follow_activity)
end
{:ok, object, meta}
end
# Tasks this handle
# - Follows if possible
# - Sends a notification
# - Generates accept or reject if appropriate
@impl true
def handle(
%{
data: %{
"id" => follow_id,
"type" => "Follow",
"object" => followed_user,
"actor" => following_user
}
} = object,
meta
) do
with %User{} = follower <- User.get_cached_by_ap_id(following_user),
%User{} = followed <- User.get_cached_by_ap_id(followed_user),
{_, {:ok, _, _}, _, _} <-
{:following, User.follow(follower, followed, :follow_pending), follower, followed} do
if followed.local && !followed.is_locked do
{:ok, accept_data, _} = Builder.accept(followed, object)
{:ok, _activity, _} = Pipeline.common_pipeline(accept_data, local: true)
end
else
{:following, {:error, _}, _follower, followed} ->
{:ok, reject_data, _} = Builder.reject(followed, object)
{:ok, _activity, _} = Pipeline.common_pipeline(reject_data, local: true)
_ ->
nil
end
{:ok, notifications} = Notification.create_notifications(object, do_send: false)
meta =
meta
|> add_notifications(notifications)
updated_object = Activity.get_by_ap_id(follow_id)
{:ok, updated_object, meta}
end
# Tasks this handles:
# - Unfollow and block
@impl true
def handle(
%{data: %{"type" => "Block", "object" => blocked_user, "actor" => blocking_user}} =
object,
meta
) do
with %User{} = blocker <- User.get_cached_by_ap_id(blocking_user),
%User{} = blocked <- User.get_cached_by_ap_id(blocked_user) do
User.block(blocker, blocked)
end
{:ok, object, meta}
end
# Tasks this handles:
# - Update the user
# - Update a non-user object (Note, Question, etc.)
#
# For a local user, we also get a changeset with the full information, so we
# can update non-federating, non-activitypub settings as well.
@impl true
def handle(%{data: %{"type" => "Update", "object" => updated_object}} = object, meta) do
updated_object_id = updated_object["id"]
with {_, true} <- {:has_id, is_binary(updated_object_id)},
%{"type" => type} <- updated_object,
{_, is_user} <- {:is_user, type in Pleroma.Constants.actor_types()} do
if is_user do
handle_update_user(object, meta)
else
handle_update_object(object, meta)
end
else
_ ->
{:ok, object, meta}
end
end
# Tasks this handles:
# - Add like to object
# - Set up notification
@impl true
def handle(%{data: %{"type" => "Like"}} = object, meta) do
liked_object = Object.get_by_ap_id(object.data["object"])
Utils.add_like_to_object(object, liked_object)
Notification.create_notifications(object)
{:ok, object, meta}
end
# Tasks this handles
# - Actually create object
# - Rollback if we couldn't create it
# - Increase the user note count
# - Increase the reply count
# - Increase replies count
# - Set up ActivityExpiration
# - Set up notifications
# - Index incoming posts for search (if needed)
@impl true
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, _user} = ActivityPub.increase_note_count_if_public(user, object)
{:ok, _user} = ActivityPub.update_last_status_at_if_public(user, object)
if in_reply_to = object.data["type"] != "Answer" && object.data["inReplyTo"] do
Object.increase_replies_count(in_reply_to)
end
if quote_url = object.data["quoteUrl"] do
Object.increase_quotes_count(quote_url)
end
reply_depth = (meta[:depth] || 0) + 1
# FIXME: Force inReplyTo to replies
if Pleroma.Web.Federator.allowed_thread_distance?(reply_depth) and
object.data["replies"] != nil do
for reply_id <- object.data["replies"] do
Pleroma.Workers.RemoteFetcherWorker.enqueue("fetch_remote", %{
"id" => reply_id,
"depth" => reply_depth
})
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)
ap_streamer().stream_out(activity)
{:ok, activity, meta}
else
e -> Repo.rollback(e)
end
end
# Tasks this handles:
# - Add announce to object
# - Set up notification
# - Stream out the announce
@impl true
def handle(%{data: %{"type" => "Announce"}} = object, meta) do
announced_object = Object.get_by_ap_id(object.data["object"])
user = User.get_cached_by_ap_id(object.data["actor"])
Utils.add_announce_to_object(object, announced_object)
if !User.internal?(user) do
Notification.create_notifications(object)
ap_streamer().stream_out(object)
end
{:ok, object, meta}
end
@impl true
def handle(%{data: %{"type" => "Undo", "object" => undone_object}} = object, meta) do
with undone_object <- Activity.get_by_ap_id(undone_object),
:ok <- handle_undoing(undone_object) do
{:ok, object, meta}
end
end
# Tasks this handles:
# - Add reaction to object
# - Set up notification
@impl true
def handle(%{data: %{"type" => "EmojiReact"}} = object, meta) 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, object, meta}
end
# Tasks this handles:
# - Delete and unpins the create activity
# - Replace object with Tombstone
# - Reduce the user note count
# - Reduce the reply count
# - Stream out the activity
# - Removes posts from search index (if needed)
@impl true
def handle(%{data: %{"type" => "Delete", "object" => deleted_object}} = object, meta) do
deleted_object =
Object.normalize(deleted_object, fetch: false) ||
User.get_cached_by_ap_id(deleted_object)
result =
case deleted_object do
%Object{} ->
with {_, {:ok, deleted_object, _activity}} <- {:object, Object.delete(deleted_object)},
{_, actor} when is_binary(actor) <- {:actor, deleted_object.data["actor"]},
{_, %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)
if in_reply_to = deleted_object.data["inReplyTo"] do
Object.decrease_replies_count(in_reply_to)
end
if quote_url = deleted_object.data["quoteUrl"] do
Object.decrease_quotes_count(quote_url)
end
MessageReference.delete_for_object(deleted_object)
ap_streamer().stream_out(object)
ap_streamer().stream_out_participations(deleted_object, user)
:ok
else
{: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{} ->
with {:ok, _} <- User.delete(deleted_object) do
:ok
end
end
if result == :ok do
# Only remove from index when deleting actual objects, not users or anything else
with %Pleroma.Object{} <- deleted_object do
Pleroma.Search.remove_from_index(deleted_object)
end
{:ok, object, meta}
else
{:error, result}
end
end
# Tasks this handles:
# - adds pin to user
# - removes expiration job for pinned activity, if was set for expiration
@impl true
def handle(%{data: %{"type" => "Add"} = data} = object, meta) do
with %User{} = user <- User.get_cached_by_ap_id(data["actor"]),
{:ok, _user} <- User.add_pinned_object_id(user, data["object"]) do
# if pinned activity was scheduled for deletion, we remove job
if expiration = Pleroma.Workers.PurgeExpiredActivity.get_expiration(meta[:activity_id]) do
Oban.cancel_job(expiration.id)
end
{:ok, object, meta}
else
nil ->
{:error, :user_not_found}
{:error, changeset} ->
if changeset.errors[:pinned_objects] do
{:error, :pinned_statuses_limit_reached}
else
changeset.errors
end
end
end
# Tasks this handles:
# - removes pin from user
# - removes corresponding Add activity
# - if activity had expiration, recreates activity expiration job
@impl true
def handle(%{data: %{"type" => "Remove"} = data} = object, meta) do
with %User{} = user <- User.get_cached_by_ap_id(data["actor"]),
{:ok, _user} <- User.remove_pinned_object_id(user, data["object"]) do
data["object"]
|> Activity.add_by_params_query(user.ap_id, user.featured_address)
|> Repo.delete_all()
# if pinned activity was scheduled for deletion, we reschedule it for deletion
if meta[:expires_at] do
# MRF.ActivityExpirationPolicy used UTC timestamps for expires_at in original implementation
{:ok, expires_at} =
Pleroma.EctoType.ActivityPub.ObjectValidators.DateTime.cast(meta[:expires_at])
Pleroma.Workers.PurgeExpiredActivity.enqueue(%{
activity_id: meta[:activity_id],
expires_at: expires_at
})
end
{:ok, object, meta}
else
nil -> {:error, :user_not_found}
error -> error
end
end
# Nothing to do
@impl true
def handle(object, meta) do
{:ok, object, meta}
end
defp handle_update_user(
%{data: %{"type" => "Update", "object" => updated_object}} = object,
meta
) do
if changeset = Keyword.get(meta, :user_update_changeset) do
changeset
|> User.update_and_set_cache()
else
{:ok, new_user_data} = ActivityPub.user_data_from_user_object(updated_object)
User.get_by_ap_id(updated_object["id"])
|> User.remote_user_changeset(new_user_data)
|> User.update_and_set_cache()
end
{:ok, object, meta}
end
defp handle_update_object(
%{data: %{"type" => "Update", "object" => updated_object}} = object,
meta
) do
orig_object_ap_id = updated_object["id"]
orig_object = Object.get_by_ap_id(orig_object_ap_id)
orig_object_data = orig_object.data
updated_object =
if meta[:local] do
# If this is a local Update, we don't process it by transmogrifier,
# so we use the embedded object as-is.
updated_object
else
meta[:object_data]
end
if orig_object_data["type"] in Pleroma.Constants.updatable_object_types() do
{:ok, _, updated} =
Object.Updater.do_update_and_invalidate_cache(orig_object, updated_object)
if updated do
object
|> Activity.normalize()
|> ActivityPub.notify_and_stream()
end
end
{:ok, object, meta}
end
def handle_object_creation(%{"type" => "ChatMessage"} = object, _activity, meta) do
with {:ok, object, meta} <- Pipeline.common_pipeline(object, meta) do
actor = User.get_cached_by_ap_id(object.data["actor"])
recipient = User.get_cached_by_ap_id(hd(object.data["to"]))
streamables =
[[actor, recipient], [recipient, actor]]
|> Enum.uniq()
|> Enum.map(fn [user, other_user] ->
if user.local do
{:ok, chat} = Chat.bump_or_create(user.id, other_user.ap_id)
{:ok, cm_ref} = MessageReference.create(chat, object, user.ap_id != actor.ap_id)
@cachex.put(
:chat_message_id_idempotency_key_cache,
cm_ref.id,
meta[:idempotency_key]
)
{
["user", "user:pleroma_chat"],
{user, %{cm_ref | chat: chat, object: object}}
}
end
end)
|> Enum.filter(& &1)
meta =
meta
|> add_streamables(streamables)
{:ok, object, meta}
end
end
def handle_object_creation(%{"type" => "Question"} = object, activity, meta) do
with {:ok, object, meta} <- Pipeline.common_pipeline(object, meta) do
PollWorker.schedule_poll_end(activity)
{:ok, object, meta}
end
end
def handle_object_creation(%{"type" => "Answer"} = object_map, _activity, meta) do
with {:ok, object, meta} <- Pipeline.common_pipeline(object_map, meta) do
Object.increase_vote_count(
object.data["inReplyTo"],
object.data["name"],
object.data["actor"]
)
{:ok, object, meta}
end
end
def handle_object_creation(%{"type" => objtype} = object, _activity, meta)
when objtype in ~w[Audio Video Image Event Article Note Page] do
with {:ok, object, meta} <- Pipeline.common_pipeline(object, meta) do
{:ok, object, meta}
end
end
# Nothing to do
def handle_object_creation(object, _activity, meta) do
{:ok, object, meta}
end
defp undo_like(nil, object), do: delete_object(object)
defp undo_like(%Object{} = liked_object, object) do
with {:ok, _} <- Utils.remove_like_from_object(object, liked_object) do
delete_object(object)
end
end
def handle_undoing(%{data: %{"type" => "Like"}} = object) do
object.data["object"]
|> Object.get_by_ap_id()
|> undo_like(object)
end
def handle_undoing(%{data: %{"type" => "EmojiReact"}} = object) do
with %Object{} = reacted_object <- Object.get_by_ap_id(object.data["object"]),
{:ok, _} <- Utils.remove_emoji_reaction_from_object(object, reacted_object),
{:ok, _} <- Repo.delete(object) do
:ok
end
end
def handle_undoing(%{data: %{"type" => "Announce"}} = object) do
with %Object{} = liked_object <- Object.get_by_ap_id(object.data["object"]),
{:ok, _} <- Utils.remove_announce_from_object(object, liked_object),
{:ok, _} <- Repo.delete(object) do
:ok
end
end
def handle_undoing(
%{data: %{"type" => "Block", "actor" => blocker, "object" => blocked}} = object
) do
with %User{} = blocker <- User.get_cached_by_ap_id(blocker),
%User{} = blocked <- User.get_cached_by_ap_id(blocked),
{:ok, _} <- User.unblock(blocker, blocked),
{:ok, _} <- Repo.delete(object) do
:ok
end
end
def handle_undoing(object), do: {:error, ["don't know how to handle", object]}
@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)
meta
end
defp send_streamables(meta) do
Keyword.get(meta, :streamables, [])
|> Enum.each(fn {topics, items} ->
Streamer.stream(topics, items)
end)
meta
end
defp add_streamables(meta, streamables) do
existing = Keyword.get(meta, :streamables, [])
meta
|> Keyword.put(:streamables, streamables ++ existing)
end
defp add_notifications(meta, notifications) do
existing = Keyword.get(meta, :notifications, [])
meta
|> Keyword.put(:notifications, notifications ++ existing)
end
@impl true
def handle_after_transaction(meta) do
meta
|> send_notifications()
|> send_streamables()
end
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex
index 4f6de8a00..83e1bee54 100644
--- a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex
@@ -1,616 +1,600 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.StatusController do
use Pleroma.Web, :controller
import Pleroma.Web.ControllerHelper,
only: [try_render: 3, add_link_headers: 2]
require Ecto.Query
alias Pleroma.Activity
alias Pleroma.Bookmark
alias Pleroma.BookmarkFolder
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.ScheduledActivity
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.ScheduledActivityView
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.Plugs.OAuthScopesPlug
alias Pleroma.Web.Plugs.RateLimiter
plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
plug(:skip_public_check when action in [:index, :show])
@unauthenticated_access %{fallback: :proceed_unauthenticated, scopes: []}
plug(
OAuthScopesPlug,
%{@unauthenticated_access | scopes: ["read:statuses"]}
when action in [
:index,
:show,
- :card,
:context,
:show_history,
:show_source
]
)
plug(
OAuthScopesPlug,
%{scopes: ["write:statuses"]}
when action in [
:create,
:delete,
:reblog,
:unreblog,
:update
]
)
plug(OAuthScopesPlug, %{scopes: ["read:favourites"]} when action == :favourites)
plug(
OAuthScopesPlug,
%{scopes: ["write:favourites"]} when action in [:favourite, :unfavourite]
)
plug(
OAuthScopesPlug,
%{scopes: ["write:mutes"]} when action in [:mute_conversation, :unmute_conversation]
)
plug(
OAuthScopesPlug,
%{@unauthenticated_access | scopes: ["read:accounts"]}
when action in [:favourited_by, :reblogged_by]
)
plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action in [:pin, :unpin])
# Note: scope not present in Mastodon: read:bookmarks
plug(OAuthScopesPlug, %{scopes: ["read:bookmarks"]} when action == :bookmarks)
# Note: scope not present in Mastodon: write:bookmarks
plug(
OAuthScopesPlug,
%{scopes: ["write:bookmarks"]} when action in [:bookmark, :unbookmark]
)
@rate_limited_status_actions ~w(reblog unreblog favourite unfavourite create delete)a
plug(
RateLimiter,
[name: :status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: [:id]]
when action in ~w(reblog unreblog)a
)
plug(
RateLimiter,
[name: :status_id_action, bucket_name: "status_id_action:fav_unfav", params: [:id]]
when action in ~w(favourite unfavourite)a
)
plug(RateLimiter, [name: :statuses_actions] when action in @rate_limited_status_actions)
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.StatusOperation
@doc """
GET `/api/v1/statuses?ids[]=1&ids[]=2`
`ids` query param is required
"""
def index(
%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{ids: ids} = params}}} =
conn,
_
) do
limit = 100
activities =
ids
|> Enum.take(limit)
|> Activity.all_by_ids_with_object()
|> Enum.filter(&Visibility.visible_for_user?(&1, user))
render(conn, "index.json",
activities: activities,
for: user,
as: :activity,
with_muted: Map.get(params, :with_muted, false)
)
end
@doc """
POST /api/v1/statuses
"""
# Creates a scheduled status when `scheduled_at` param is present and it's far enough
def create(
%{
assigns: %{user: user},
private: %{
open_api_spex: %{body_params: %{status: _, scheduled_at: scheduled_at} = params}
}
} = conn,
_
)
when not is_nil(scheduled_at) do
params =
Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
|> put_application(conn)
attrs = %{
params: Map.new(params, fn {key, value} -> {to_string(key), value} end),
scheduled_at: scheduled_at
}
with {:far_enough, true} <- {:far_enough, ScheduledActivity.far_enough?(scheduled_at)},
{:ok, scheduled_activity} <- ScheduledActivity.create(user, attrs) do
conn
|> put_view(ScheduledActivityView)
|> render("show.json", scheduled_activity: scheduled_activity)
else
{:far_enough, _} ->
params = Map.drop(params, [:scheduled_at])
put_in(
conn,
[Access.key(:private), Access.key(:open_api_spex), Access.key(:body_params)],
params
)
|> do_create
error ->
error
end
end
# Creates a regular status
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)
with {:ok, activity} <- CommonAPI.post(user, params) do
try_render(conn, "show.json",
activity: activity,
for: user,
as: :activity,
with_direct_conversation_id: true
)
else
{:error, {:reject, message}} ->
conn
|> put_status(:unprocessable_entity)
|> json(%{error: message})
{:error, message} ->
conn
|> put_status(:unprocessable_entity)
|> json(%{error: message})
end
end
@doc "GET /api/v1/statuses/:id/history"
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
try_render(conn, "history.json",
activity: activity,
for: user,
with_direct_conversation_id: true,
with_muted: Map.get(params, :with_muted, false)
)
else
_ -> {:error, :not_found}
end
end
@doc "GET /api/v1/statuses/:id/source"
def show_source(%{assigns: assigns, 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
try_render(conn, "source.json",
activity: activity,
for: user
)
else
_ -> {:error, :not_found}
end
end
@doc "PUT /api/v1/statuses/:id"
def update(
%{
assigns: %{user: user},
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"},
actor <- Activity.user_actor(activity),
{_, true} <- {:own_status, actor.id == user.id},
changes <- body_params |> put_application(conn),
{_, {:ok, _update_activity}} <- {:pipeline, CommonAPI.update(user, activity, changes)},
{_, %Activity{}} = {_, activity} <- {:refetched, Activity.get_by_id_with_object(id)} do
try_render(conn, "show.json",
activity: activity,
for: user,
with_direct_conversation_id: true,
with_muted: Map.get(params, :with_muted, false)
)
else
{:own_status, _} -> {:error, :forbidden}
{:pipeline, _} -> {:error, :internal_server_error}
_ -> {:error, :not_found}
end
end
@doc "GET /api/v1/statuses/:id"
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",
activity: activity,
for: user,
with_direct_conversation_id: true,
with_muted: Map.get(params, :with_muted, false)
)
else
_ -> {:error, :not_found}
end
end
@doc "DELETE /api/v1/statuses/:id"
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",
activity: activity,
for: user,
with_direct_conversation_id: true,
with_source: true
)
else
_e -> {:error, :not_found}
end
end
@doc "POST /api/v1/statuses/:id/reblog"
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})
end
end
@doc "POST /api/v1/statuses/:id/unreblog"
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})
end
end
@doc "POST /api/v1/statuses/:id/favourite"
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)
end
end
@doc "POST /api/v1/statuses/:id/unfavourite"
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)
end
end
@doc "POST /api/v1/statuses/:id/pin"
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
{:error, :pinned_statuses_limit_reached} ->
{:error, "You have already pinned the maximum number of statuses"}
{:error, :ownership_error} ->
{:error, :unprocessable_entity, "Someone else's status cannot be pinned"}
{:error, :visibility_error} ->
{:error, :unprocessable_entity, "Non-public status cannot be pinned"}
error ->
error
end
end
@doc "POST /api/v1/statuses/:id/unpin"
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},
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),
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}, 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),
{:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
end
end
@doc "POST /api/v1/statuses/:id/mute"
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)
end
end
@doc "POST /api/v1/statuses/:id/unmute"
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}, private: %{open_api_spex: %{params: %{id: status_id}}}} = conn,
- _
- ) 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}, 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)},
%Object{data: %{"likes" => likes}} <- Object.normalize(activity, fetch: false) do
users =
User
|> Ecto.Query.where([u], u.ap_id in ^likes)
|> Repo.all()
|> Enum.filter(&(not User.blocks?(user, &1)))
conn
|> put_view(AccountView)
|> render("index.json", for: user, users: users, as: :user)
else
{:visible, false} -> {:error, :not_found}
_ -> json(conn, [])
end
end
@doc "GET /api/v1/statuses/:id/reblogged_by"
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}} <-
Object.normalize(activity, fetch: false) do
announces =
"Announce"
|> Activity.Queries.by_type()
|> Ecto.Query.where([a], a.actor in ^announces)
# this is to use the index
|> Activity.Queries.by_object_id(ap_id)
|> Repo.all()
|> Enum.filter(&Visibility.visible_for_user?(&1, user))
|> Enum.map(& &1.actor)
|> Enum.uniq()
users =
User
|> Ecto.Query.where([u], u.ap_id in ^announces)
|> Repo.all()
|> Enum.filter(&(not User.blocks?(user, &1)))
conn
|> put_view(AccountView)
|> render("index.json", for: user, users: users, as: :user)
else
{:visible, false} -> {:error, :not_found}
_ -> json(conn, [])
end
end
@doc "GET /api/v1/statuses/:id/context"
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"], %{
blocking_user: user,
user: user,
exclude_id: activity.id
})
render(conn, "context.json", activity: activity, activities: activities, user: user)
end
end
@doc "GET /api/v1/favourites"
def favourites(
%{assigns: %{user: %User{} = user}, private: %{open_api_spex: %{params: params}}} = conn,
_
) do
activities = ActivityPub.fetch_favourites(user, params)
conn
|> add_link_headers(activities)
|> render("index.json",
activities: activities,
for: user,
as: :activity
)
end
@doc "GET /api/v1/bookmarks"
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(folder_id)
|> Pleroma.Pagination.fetch_paginated(params)
activities =
bookmarks
|> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
conn
|> add_link_headers(bookmarks)
|> render("index.json",
activities: activities,
for: user,
as: :activity
)
end
defp put_application(params, %{assigns: %{token: %Token{user: %User{} = user} = token}} = _conn) do
if user.disclose_client do
%{client_name: client_name, website: website} = Repo.preload(token, :app).app
Map.put(params, :generator, %{type: "Application", name: client_name, url: website})
else
Map.put(params, :generator, nil)
end
end
defp put_application(params, _), do: Map.put(params, :generator, nil)
end
diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex
index e464f60dc..77af69eef 100644
--- a/lib/pleroma/web/mastodon_api/views/status_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/status_view.ex
@@ -1,842 +1,836 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.StatusView do
use Pleroma.Web, :view
require Pleroma.Constants
alias Pleroma.Activity
alias Pleroma.HTML
alias Pleroma.Maps
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.UserRelationship
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.PollView
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]
# This is a naive way to do this, just spawning a process per activity
# to fetch the preview. However it should be fine considering
# 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
# TODO: Add cached version.
defp get_replied_to_activities([]), do: %{}
defp get_replied_to_activities(activities) do
activities
|> Enum.map(fn
%{data: %{"type" => "Create"}} = activity ->
object = Object.normalize(activity, fetch: false)
object && object.data["inReplyTo"] != "" && object.data["inReplyTo"]
_ ->
nil
end)
|> Enum.filter(& &1)
|> Activity.create_by_object_ap_id_with_object()
|> Repo.all()
|> Enum.reduce(%{}, fn activity, acc ->
object = Object.normalize(activity, fetch: false)
if object, do: Map.put(acc, object.data["id"], activity), else: acc
end)
end
defp get_quoted_activities([]), do: %{}
defp get_quoted_activities(activities) do
activities
|> Enum.map(fn
%{data: %{"type" => "Create"}} = activity ->
object = Object.normalize(activity, fetch: false)
object && object.data["quoteUrl"] != "" && object.data["quoteUrl"]
_ ->
nil
end)
|> Enum.filter(& &1)
|> Activity.create_by_object_ap_id_with_object()
|> Repo.all()
|> Enum.reduce(%{}, fn activity, acc ->
object = Object.normalize(activity, fetch: false)
if object, do: Map.put(acc, object.data["id"], activity), else: acc
end)
end
# DEPRECATED This field seems to be a left-over from the StatusNet era.
# If your application uses `pleroma.conversation_id`: this field is deprecated.
# It is currently stubbed instead by doing a CRC32 of the context, and
# clearing the MSB to avoid overflow exceptions with signed integers on the
# different clients using this field (Java/Kotlin code, mostly; see Husky.)
# This should be removed in a future version of Pleroma. Pleroma-FE currently
# depends on this field, as well.
defp get_context_id(%{data: %{"context" => context}}) when is_binary(context) do
import Bitwise
:erlang.crc32(context)
|> band(bnot(0x8000_0000))
end
defp get_context_id(_), do: nil
# Check if the user reblogged this status
defp reblogged?(activity, %User{ap_id: ap_id}) do
with %Object{data: %{"announcements" => announcements}} when is_list(announcements) <-
Object.normalize(activity, fetch: false) do
ap_id in announcements
else
_ -> false
end
end
# False if the user is logged out
defp reblogged?(_activity, _user), do: false
def render("index.json", opts) do
reading_user = opts[:for]
# 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)
parent_activities =
activities
|> Enum.filter(&(&1.data["type"] == "Announce" && &1.data["object"]))
|> Enum.map(&Object.normalize(&1, fetch: false).data["id"])
|> Activity.create_by_object_ap_id()
|> Activity.with_preloaded_object(:left)
|> Activity.with_preloaded_bookmark(reading_user)
|> Activity.with_set_thread_muted_field(reading_user)
|> Repo.all()
relationships_opt =
cond do
Map.has_key?(opts, :relationships) ->
opts[:relationships]
is_nil(reading_user) ->
UserRelationship.view_relationships_option(nil, [])
true ->
# Note: unresolved users are filtered out
actors =
(activities ++ parent_activities)
|> Enum.map(&CommonAPI.get_user(&1.data["actor"], false))
|> Enum.filter(& &1)
UserRelationship.view_relationships_option(reading_user, actors, subset: :source_mutes)
end
opts =
opts
|> Map.put(:replied_to_activities, replied_to_activities)
|> Map.put(:quoted_activities, quoted_activities)
|> Map.put(:parent_activities, parent_activities)
|> Map.put(:relationships, relationships_opt)
safe_render_many(activities, StatusView, "show.json", opts)
end
def render(
"show.json",
%{activity: %{data: %{"type" => "Announce", "object" => _object}} = activity} = opts
) do
user = CommonAPI.get_user(activity.data["actor"])
created_at = Utils.to_masto_date(activity.data["published"])
object = Object.normalize(activity, fetch: false)
reblogged_parent_activity =
if opts[:parent_activities] do
Activity.Queries.find_by_object_ap_id(
opts[:parent_activities],
object.data["id"]
)
else
Activity.create_by_object_ap_id(object.data["id"])
|> Activity.with_preloaded_bookmark(opts[:for])
|> Activity.with_set_thread_muted_field(opts[:for])
|> Repo.one()
end
reblog_rendering_opts = Map.put(opts, :activity, reblogged_parent_activity)
reblogged = render("show.json", reblog_rendering_opts)
favorited = opts[:for] && opts[:for].ap_id in (object.data["likes"] || [])
bookmark = Activity.get_bookmark(reblogged_parent_activity, opts[:for])
bookmark_folder =
if bookmark != nil do
bookmark.folder_id
else
nil
end
mentions =
activity.recipients
|> Enum.map(fn ap_id -> User.get_cached_by_ap_id(ap_id) end)
|> Enum.filter(& &1)
|> Enum.map(fn user -> AccountView.render("mention.json", %{user: user}) end)
{pinned?, pinned_at} = pin_data(object, user)
%{
id: to_string(activity.id),
uri: object.data["id"],
url: object.data["id"],
account:
AccountView.render("show.json", %{
user: user,
for: opts[:for]
}),
in_reply_to_id: nil,
in_reply_to_account_id: nil,
reblog: reblogged,
content: reblogged[:content] || "",
created_at: created_at,
reblogs_count: 0,
replies_count: 0,
favourites_count: 0,
reblogged: reblogged?(reblogged_parent_activity, opts[:for]),
favourited: present?(favorited),
bookmarked: present?(bookmark),
muted: false,
pinned: pinned?,
sensitive: false,
spoiler_text: "",
visibility: get_visibility(activity),
media_attachments: reblogged[:media_attachments] || [],
mentions: mentions,
tags: reblogged[:tags] || [],
application: build_application(object.data["generator"]),
language: nil,
emojis: [],
pleroma: %{
local: activity.local,
pinned_at: pinned_at,
bookmark_folder: bookmark_folder
}
}
end
def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do
object = Object.normalize(activity, fetch: false)
user = CommonAPI.get_user(activity.data["actor"])
user_follower_address = user.follower_address
like_count = object.data["like_count"] || 0
announcement_count = object.data["announcement_count"] || 0
hashtags = Object.hashtags(object)
sensitive = object.data["sensitive"] || Enum.member?(hashtags, "nsfw")
tags = Object.tags(object)
tag_mentions =
tags
|> Enum.filter(fn tag -> is_map(tag) and tag["type"] == "Mention" end)
|> Enum.map(fn tag -> tag["href"] end)
mentions =
(object.data["to"] ++ tag_mentions)
|> Enum.uniq()
|> Enum.map(fn
Pleroma.Constants.as_public() -> nil
^user_follower_address -> nil
ap_id -> User.get_cached_by_ap_id(ap_id)
end)
|> Enum.filter(& &1)
|> Enum.map(fn user -> AccountView.render("mention.json", %{user: user}) end)
favorited = opts[:for] && opts[:for].ap_id in (object.data["likes"] || [])
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
expires_at =
with true <- client_posted_this_activity,
%Oban.Job{scheduled_at: scheduled_at} <-
Pleroma.Workers.PurgeExpiredActivity.get_expiration(activity.id) do
scheduled_at
else
_ -> nil
end
thread_muted? =
cond do
is_nil(opts[:for]) -> false
is_boolean(activity.thread_muted?) -> activity.thread_muted?
true -> CommonAPI.thread_muted?(opts[:for], activity)
end
attachment_data = object.data["attachment"] || []
attachments = render_many(attachment_data, StatusView, "attachment.json", as: :attachment)
created_at = Utils.to_masto_date(object.data["published"])
edited_at =
with %{"updated" => updated} <- object.data,
date <- Utils.to_masto_date(updated),
true <- date != "" do
date
else
_ ->
nil
end
reply_to = get_reply_to(activity, opts)
reply_to_user = reply_to && CommonAPI.get_user(reply_to.data["actor"])
history_len =
1 +
(Object.Updater.history_for(object.data)
|> Map.get("orderedItems")
|> length())
# See render("history.json", ...) for more details
# Here the implicit index of the current content is 0
chrono_order = history_len - 1
quote_activity = get_quote(activity, opts)
quote_id =
case quote_activity do
%Activity{id: id} -> id
_ -> nil
end
quote_post =
if visible_for_user?(quote_activity, opts[:for]) and opts[:show_quote] != false do
quote_rendering_opts = Map.merge(opts, %{activity: quote_activity, show_quote: false})
render("show.json", quote_rendering_opts)
else
nil
end
content =
object
|> render_content()
content_html =
content
|> Activity.HTML.get_cached_scrubbed_html_for_activity(
User.html_filter_policy(opts[:for]),
activity,
"mastoapi:content:#{chrono_order}"
)
content_plaintext =
content
|> Activity.HTML.get_cached_stripped_html_for_activity(
activity,
"mastoapi:content:#{chrono_order}"
)
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
Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, activity)
else
object.data["url"] || object.data["external_url"] || object.data["id"]
end
direct_conversation_id =
with {_, nil} <- {:direct_conversation_id, opts[:direct_conversation_id]},
{_, true} <- {:include_id, opts[:with_direct_conversation_id]},
{_, %User{} = for_user} <- {:for_user, opts[:for]} do
Activity.direct_conversation_id(activity, for_user)
else
{:direct_conversation_id, participation_id} when is_integer(participation_id) ->
participation_id
_e ->
nil
end
emoji_reactions =
object
|> Object.get_emoji_reactions()
|> EmojiReactionController.filter_allowed_users(
opts[:for],
Map.get(opts, :with_muted, false)
)
|> Stream.map(fn {emoji, users, url} ->
build_emoji_map(emoji, users, url, opts[:for])
end)
|> Enum.to_list()
# Status muted state (would do 1 request per status unless user mutes are preloaded)
muted =
thread_muted? ||
UserRelationship.exists?(
get_in(opts, [:relationships, :user_relationships]),
:mute,
opts[:for],
user,
fn for_user, user -> User.mutes?(for_user, user) end
)
{pinned?, pinned_at} = pin_data(object, user)
%{
id: to_string(activity.id),
uri: object.data["id"],
url: url,
account:
AccountView.render("show.json", %{
user: user,
for: opts[:for]
}),
in_reply_to_id: reply_to && to_string(reply_to.id),
in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id),
reblog: nil,
card: card,
content: content_html,
text: opts[:with_source] && get_source_text(object.data["source"]),
created_at: created_at,
edited_at: edited_at,
reblogs_count: announcement_count,
replies_count: object.data["repliesCount"] || 0,
favourites_count: like_count,
reblogged: reblogged?(activity, opts[:for]),
favourited: present?(favorited),
bookmarked: present?(bookmark),
muted: muted,
pinned: pinned?,
sensitive: sensitive,
spoiler_text: summary,
visibility: get_visibility(object),
media_attachments: attachments,
poll: render(PollView, "show.json", object: object, for: opts[:for]),
mentions: mentions,
tags: build_tags(tags),
application: build_application(object.data["generator"]),
language: nil,
emojis: build_emojis(object.data["emoji"]),
pleroma: %{
local: activity.local,
conversation_id: get_context_id(activity),
context: object.data["context"],
in_reply_to_account_acct: reply_to_user && reply_to_user.nickname,
quote: quote_post,
quote_id: quote_id,
quote_url: object.data["quoteUrl"],
quote_visible: visible_for_user?(quote_activity, opts[:for]),
content: %{"text/plain" => content_plaintext},
spoiler_text: %{"text/plain" => summary},
expires_at: expires_at,
direct_conversation_id: direct_conversation_id,
thread_muted: thread_muted?,
emoji_reactions: emoji_reactions,
parent_visible: visible_for_user?(reply_to, opts[:for]),
pinned_at: pinned_at,
quotes_count: object.data["quotesCount"] || 0,
bookmark_folder: bookmark_folder
}
}
end
def render("show.json", _) do
nil
end
def render("history.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do
object = Object.normalize(activity, fetch: false)
hashtags = Object.hashtags(object)
user = CommonAPI.get_user(activity.data["actor"])
past_history =
Object.Updater.history_for(object.data)
|> Map.get("orderedItems")
|> Enum.map(&Map.put(&1, "id", object.data["id"]))
|> Enum.map(&%Object{data: &1, id: object.id})
history =
[object | past_history]
# Mastodon expects the original to be at the first
|> Enum.reverse()
|> Enum.with_index()
|> Enum.map(fn {object, chrono_order} ->
%{
# The history is prepended every time there is a new edit.
# In chrono_order, the oldest item is always at 0, and so on.
# The chrono_order is an invariant kept between edits.
chrono_order: chrono_order,
object: object
}
end)
individual_opts =
opts
|> Map.put(:as, :item)
|> Map.put(:user, user)
|> Map.put(:hashtags, hashtags)
render_many(history, StatusView, "history_item.json", individual_opts)
end
def render(
"history_item.json",
%{
activity: activity,
user: user,
item: %{object: object, chrono_order: chrono_order},
hashtags: hashtags
} = opts
) do
sensitive = object.data["sensitive"] || Enum.member?(hashtags, "nsfw")
attachment_data = object.data["attachment"] || []
attachments = render_many(attachment_data, StatusView, "attachment.json", as: :attachment)
created_at = Utils.to_masto_date(object.data["updated"] || object.data["published"])
content =
object
|> render_content()
content_html =
content
|> Activity.HTML.get_cached_scrubbed_html_for_activity(
User.html_filter_policy(opts[:for]),
activity,
"mastoapi:content:#{chrono_order}"
)
summary = object.data["summary"] || ""
%{
account:
AccountView.render("show.json", %{
user: user,
for: opts[:for]
}),
content: content_html,
sensitive: sensitive,
spoiler_text: summary,
created_at: created_at,
media_attachments: attachments,
emojis: build_emojis(object.data["emoji"]),
poll: render(PollView, "show.json", object: object, for: opts[:for])
}
end
def render("source.json", %{activity: %{data: %{"object" => _object}} = activity} = _opts) do
object = Object.normalize(activity, fetch: false)
%{
id: activity.id,
text: get_source_text(Map.get(object.data, "source", "")),
spoiler_text: Map.get(object.data, "summary", ""),
content_type: get_source_content_type(object.data["source"])
}
end
- def render("card.json", %{rich_media: rich_media, page_url: page_url}) do
- 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
image_url = proxied_url(rich_media["image"], page_url_data)
audio_url = proxied_url(rich_media["audio"], page_url_data)
video_url = proxied_url(rich_media["video"], page_url_data)
%{
type: "link",
provider_name: page_url_data.host,
provider_url: page_url_data.scheme <> "://" <> page_url_data.host,
url: page_url,
image: image_url,
title: rich_media["title"] || "",
description: rich_media["description"] || "",
pleroma: %{
opengraph:
rich_media
|> Maps.put_if_present("image", image_url)
|> Maps.put_if_present("audio", audio_url)
|> Maps.put_if_present("video", video_url)
}
}
end
def render("card.json", _), do: nil
def render("attachment.json", %{attachment: attachment}) do
[attachment_url | _] = attachment["url"]
media_type = attachment_url["mediaType"] || attachment_url["mimeType"] || "image"
href = attachment_url["href"] |> MediaProxy.url()
href_preview = attachment_url["href"] |> MediaProxy.preview_url()
meta = render("attachment_meta.json", %{attachment: attachment})
type =
cond do
String.contains?(media_type, "image") -> "image"
String.contains?(media_type, "video") -> "video"
String.contains?(media_type, "audio") -> "audio"
true -> "unknown"
end
attachment_id =
with {_, ap_id} when is_binary(ap_id) <- {:ap_id, attachment["id"]},
{_, %Object{data: _object_data, id: object_id}} <-
{:object, Object.get_by_ap_id(ap_id)} do
to_string(object_id)
else
_ ->
<<hash_id::signed-32, _rest::binary>> = :crypto.hash(:md5, href)
to_string(attachment["id"] || hash_id)
end
%{
id: attachment_id,
url: href,
remote_url: href,
preview_url: href_preview,
text_url: href,
type: type,
description: attachment["name"],
pleroma: %{mime_type: media_type},
blurhash: attachment["blurhash"]
}
|> Maps.put_if_present(:meta, meta)
end
def render("attachment_meta.json", %{
attachment: %{"url" => [%{"width" => width, "height" => height} | _]}
})
when is_integer(width) and is_integer(height) do
%{
original: %{
width: width,
height: height,
aspect: width / height
}
}
end
def render("attachment_meta.json", _), do: nil
def render("context.json", %{activity: activity, activities: activities, user: user}) do
%{ancestors: ancestors, descendants: descendants} =
activities
|> Enum.reverse()
|> Enum.group_by(fn %{id: id} -> if id < activity.id, do: :ancestors, else: :descendants end)
|> Map.put_new(:ancestors, [])
|> Map.put_new(:descendants, [])
%{
ancestors: render("index.json", for: user, activities: ancestors, as: :activity),
descendants: render("index.json", for: user, activities: descendants, as: :activity)
}
end
def get_reply_to(activity, %{replied_to_activities: replied_to_activities}) do
object = Object.normalize(activity, fetch: false)
with nil <- replied_to_activities[object.data["inReplyTo"]] do
# If user didn't participate in the thread
Activity.get_in_reply_to_activity(activity)
end
end
def get_reply_to(%{data: %{"object" => _object}} = activity, _) do
object = Object.normalize(activity, fetch: false)
if object.data["inReplyTo"] && object.data["inReplyTo"] != "" do
Activity.get_create_by_object_ap_id(object.data["inReplyTo"])
else
nil
end
end
def get_quote(activity, %{quoted_activities: quoted_activities}) do
object = Object.normalize(activity, fetch: false)
with nil <- quoted_activities[object.data["quoteUrl"]] do
# For when a quote post is inside an Announce
Activity.get_create_by_object_ap_id_with_object(object.data["quoteUrl"])
end
end
def get_quote(%{data: %{"object" => _object}} = activity, _) do
object = Object.normalize(activity, fetch: false)
if object.data["quoteUrl"] && object.data["quoteUrl"] != "" do
Activity.get_create_by_object_ap_id(object.data["quoteUrl"])
else
nil
end
end
def render_content(%{data: %{"name" => name}} = object) when not is_nil(name) and name != "" do
url = object.data["url"] || object.data["id"]
"<p><a href=\"#{url}\">#{name}</a></p>#{object.data["content"]}"
end
def render_content(object), do: object.data["content"] || ""
@doc """
Builds a dictionary tags.
## Examples
iex> Pleroma.Web.MastodonAPI.StatusView.build_tags(["fediverse", "nextcloud"])
[{"name": "fediverse", "url": "/tag/fediverse"},
{"name": "nextcloud", "url": "/tag/nextcloud"}]
"""
@spec build_tags(list(any())) :: list(map())
def build_tags(object_tags) when is_list(object_tags) do
object_tags
|> Enum.filter(&is_binary/1)
|> Enum.map(&%{name: &1, url: "#{Pleroma.Web.Endpoint.url()}/tag/#{URI.encode(&1)}"})
end
def build_tags(_), do: []
@doc """
Builds list emojis.
Arguments: `nil` or list tuple of name and url.
Returns list emojis.
## Examples
iex> Pleroma.Web.MastodonAPI.StatusView.build_emojis([{"2hu", "corndog.png"}])
[%{shortcode: "2hu", static_url: "corndog.png", url: "corndog.png", visible_in_picker: false}]
"""
@spec build_emojis(nil | list(tuple())) :: list(map())
def build_emojis(nil), do: []
def build_emojis(emojis) do
emojis
|> Enum.map(fn {name, url} ->
name = HTML.strip_tags(name)
url =
url
|> HTML.strip_tags()
|> MediaProxy.url()
%{shortcode: name, url: url, static_url: url, visible_in_picker: false}
end)
end
defp present?(nil), do: false
defp present?(false), do: false
defp present?(_), do: true
defp pin_data(%Object{data: %{"id" => object_id}}, %User{pinned_objects: pinned_objects}) do
if pinned_at = pinned_objects[object_id] do
{true, Utils.to_masto_date(pinned_at)}
else
{false, nil}
end
end
defp build_emoji_map(emoji, users, url, current_user) do
%{
name: Pleroma.Web.PleromaAPI.EmojiReactionView.emoji_name(emoji, url),
count: length(users),
url: MediaProxy.url(url),
me: !!(current_user && current_user.ap_id in users),
account_ids: Enum.map(users, fn user -> User.get_cached_by_ap_id(user).id end)
}
end
@spec build_application(map() | nil) :: map() | nil
defp build_application(%{"type" => _type, "name" => name, "url" => url}),
do: %{name: name, website: url}
defp build_application(_), do: nil
# Workaround for Elixir issue #10771
# Avoid applying URI.merge unless necessary
# TODO: revert to always attempting URI.merge(image_url_data, page_url_data)
# when Elixir 1.12 is the minimum supported version
@spec build_image_url(struct() | nil, struct()) :: String.t() | nil
defp build_image_url(
%URI{scheme: image_scheme, host: image_host} = image_url_data,
%URI{} = _page_url_data
)
when not is_nil(image_scheme) and not is_nil(image_host) do
image_url_data |> to_string
end
defp build_image_url(%URI{} = image_url_data, %URI{} = page_url_data) do
URI.merge(page_url_data, image_url_data) |> to_string
end
defp get_source_text(%{"content" => content} = _source) do
content
end
defp get_source_text(source) when is_binary(source) do
source
end
defp get_source_text(_) do
""
end
defp get_source_content_type(%{"mediaType" => type} = _source) do
type
end
defp get_source_content_type(_source) do
Utils.get_content_type(nil)
end
defp proxied_url(url, page_url_data) do
if is_binary(url) do
build_image_url(URI.parse(url), page_url_data) |> MediaProxy.url()
else
nil
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
index 241bf0010..a1c88d075 100644
--- a/lib/pleroma/web/pleroma_api/views/chat/message_reference_view.ex
+++ b/lib/pleroma/web/pleroma_api/views/chat/message_reference_view.ex
@@ -1,63 +1,66 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.PleromaAPI.Chat.MessageReferenceView do
use Pleroma.Web, :view
alias Pleroma.Maps
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)
def render(
"show.json",
%{
chat_message_reference: %{
id: id,
object: %{data: chat_message} = object,
chat_id: chat_id,
unread: unread
}
}
) 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"],
chat_id: chat_id |> to_string(),
account_id: User.get_cached_by_ap_id(chat_message["actor"]).id,
created_at: Utils.to_masto_date(chat_message["published"]),
emojis: StatusView.build_emojis(chat_message["emoji"]),
attachment:
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
def render("index.json", opts) do
render_many(
opts[:chat_message_references],
__MODULE__,
"show.json",
Map.put(opts, :as, :chat_message_reference)
)
end
defp put_idempotency_key(data) do
with {:ok, idempotency_key} <- @cachex.get(:chat_message_id_idempotency_key_cache, data.id) do
data
|> Maps.put_if_present(:idempotency_key, idempotency_key)
else
_ -> data
end
end
end
diff --git a/lib/pleroma/web/rich_media/backfill.ex b/lib/pleroma/web/rich_media/backfill.ex
new file mode 100644
index 000000000..4ec50e132
--- /dev/null
+++ 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
new file mode 100644
index 000000000..36a1ae44a
--- /dev/null
+++ 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
index a711dc436..119994458 100644
--- a/lib/pleroma/web/rich_media/helpers.ex
+++ b/lib/pleroma/web/rich_media/helpers.ex
@@ -1,109 +1,65 @@
# 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.Helpers do
- alias Pleroma.Activity
- alias Pleroma.HTML
- alias Pleroma.Object
- alias Pleroma.Web.RichMedia.Parser
-
- @cachex Pleroma.Config.get([:cachex, :provider], Cachex)
-
- @config_impl Application.compile_env(:pleroma, [__MODULE__, :config_impl], Pleroma.Config)
-
- @options [
- pool: :media,
- max_body: 2_000_000,
- recv_timeout: 2_000
- ]
-
- 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, 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
- if object.data["fake"] do
- fetch_data_for_object(object)
- else
- key = "URL|#{activity.id}"
-
- @cachex.fetch!(:scrubber_cache, key, fn _ ->
- result = fetch_data_for_object(object)
-
- cond do
- match?(%{page_url: _, rich_media: _}, result) ->
- Activity.HTML.add_cache_key_for(activity.id, key)
- {:commit, result}
-
- true ->
- {:ignore, %{}}
- end
- end)
- end
- 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 ->
e
{:ok, %Tesla.Env{status: 200, headers: headers}} ->
with :ok <- check_content_type(headers),
:ok <- check_content_length(headers),
do: :ok
_ ->
: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
case List.keyfind(headers, "content-type", 0) do
{_, content_type} ->
case Plug.Conn.Utils.media_type(content_type) do
{:ok, "text", "html", _} -> :ok
_ -> {:error, {:content_type, content_type}}
end
_ ->
:ok
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
_ ->
: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
index a73fbc4b9..37cf29029 100644
--- a/lib/pleroma/web/rich_media/parser.ex
+++ b/lib/pleroma/web/rich_media/parser.ex
@@ -1,208 +1,102 @@
# 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 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"}
+ def parse(nil), do: nil
@spec parse(String.t()) :: {:ok, map()} | {:error, any()}
def parse(url) do
with :ok <- validate_page_url(url),
- {:ok, data} <- get_cached_or_parse(url),
- {:ok, _} <- set_ttl_based_on_image(data, url) do
+ {:ok, data} <- parse_url(url) do
+ data = Map.put(data, "url", url)
{: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
-
- @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
-
- Define the module in the config
-
- config :pleroma, :rich_media,
- ttl_setters: [MyModule]
- """
- @spec set_ttl_based_on_image(map(), String.t()) ::
- {:ok, integer() | :noop} | {:error, :no_key}
- def set_ttl_based_on_image(data, url) do
- case get_ttl_from_image(data, url) do
- 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}
- 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
end
defp maybe_parse(html) do
Enum.reduce_while(parsers(), %{}, fn parser, acc ->
case parser.parse(html, acc) do
data when data != %{} -> {:halt, data}
_ -> {:cont, acc}
end
end)
end
defp check_parsed_data(%{"title" => title} = data)
when is_binary(title) and title != "" do
{:ok, data}
end
defp check_parsed_data(data) do
{:error, {:invalid_metadata, data}}
end
defp clean_parsed_data(data) do
data
|> Enum.reject(fn {key, val} ->
not match?({:ok, _}, Jason.encode(%{key => val}))
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
index b51298bd8..7e56375ff 100644
--- a/lib/pleroma/web/rich_media/parser/ttl.ex
+++ b/lib/pleroma/web/rich_media/parser/ttl.ex
@@ -1,7 +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 do
@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
index a0d567c42..948c727e1 100644
--- a/lib/pleroma/web/rich_media/parser/ttl/aws_signed_url.ex
+++ b/lib/pleroma/web/rich_media/parser/ttl/aws_signed_url.ex
@@ -1,50 +1,51 @@
# 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.AwsSignedUrl do
@behaviour Pleroma.Web.RichMedia.Parser.TTL
@impl true
def ttl(data, _url) do
- image = Map.get(data, :image)
+ image = Map.get(data, "image")
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 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 aws_signed_url?(_), do: nil
defp parse_query_params(image) do
%URI{query: query} = URI.parse(image)
query
end
defp format_query_params(query) do
query
|> String.split(~r/&|=/)
|> Enum.chunk_every(2)
|> Map.new(fn [k, v] -> {k, v} end)
end
defp get_expiration_timestamp(params) when is_map(params) do
{:ok, date} =
params
|> Map.get("X-Amz-Date")
|> Timex.parse("{ISO:Basic:Z}")
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
new file mode 100644
index 000000000..b06889669
--- /dev/null
+++ 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
index 4fe0cb02f..86d6da883 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -1,1033 +1,1032 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Router do
use Pleroma.Web, :router
import Phoenix.LiveDashboard.Router
pipeline :accepts_html do
plug(:accepts, ["html"])
end
pipeline :accepts_html_xml do
plug(:accepts, ["html", "xml", "rss", "atom"])
end
pipeline :accepts_html_json do
plug(:accepts, ["html", "activity+json", "json"])
end
pipeline :accepts_html_xml_json do
plug(:accepts, ["html", "xml", "rss", "atom", "activity+json", "json"])
end
pipeline :accepts_xml_rss_atom do
plug(:accepts, ["xml", "rss", "atom"])
end
pipeline :browser do
plug(:accepts, ["html"])
plug(:fetch_session)
end
pipeline :oauth do
plug(:fetch_session)
plug(Pleroma.Web.Plugs.OAuthPlug)
plug(Pleroma.Web.Plugs.UserEnabledPlug)
plug(Pleroma.Web.Plugs.EnsureUserTokenAssignsPlug)
end
# Note: expects _user_ authentication (user-unbound app-bound tokens don't qualify)
pipeline :expect_user_authentication do
plug(Pleroma.Web.Plugs.ExpectAuthenticatedCheckPlug)
end
# Note: expects public instance or _user_ authentication (user-unbound tokens don't qualify)
pipeline :expect_public_instance_or_user_authentication do
plug(Pleroma.Web.Plugs.ExpectPublicOrAuthenticatedCheckPlug)
end
pipeline :authenticate do
plug(Pleroma.Web.Plugs.OAuthPlug)
plug(Pleroma.Web.Plugs.BasicAuthDecoderPlug)
plug(Pleroma.Web.Plugs.UserFetcherPlug)
plug(Pleroma.Web.Plugs.AuthenticationPlug)
end
pipeline :after_auth do
plug(Pleroma.Web.Plugs.UserEnabledPlug)
plug(Pleroma.Web.Plugs.SetUserSessionIdPlug)
plug(Pleroma.Web.Plugs.EnsureUserTokenAssignsPlug)
plug(Pleroma.Web.Plugs.UserTrackingPlug)
end
pipeline :base_api do
plug(:accepts, ["json"])
plug(:fetch_session)
plug(:authenticate)
plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)
end
pipeline :no_auth_or_privacy_expectations_api do
plug(:base_api)
plug(:after_auth)
plug(Pleroma.Web.Plugs.IdempotencyPlug)
end
# Pipeline for app-related endpoints (no user auth checks — app-bound tokens must be supported)
pipeline :app_api do
plug(:no_auth_or_privacy_expectations_api)
end
pipeline :api do
plug(:expect_public_instance_or_user_authentication)
plug(:no_auth_or_privacy_expectations_api)
end
pipeline :authenticated_api do
plug(:expect_user_authentication)
plug(:no_auth_or_privacy_expectations_api)
plug(Pleroma.Web.Plugs.EnsureAuthenticatedPlug)
end
pipeline :admin_api do
plug(:expect_user_authentication)
plug(:base_api)
plug(Pleroma.Web.Plugs.AdminSecretAuthenticationPlug)
plug(:after_auth)
plug(Pleroma.Web.Plugs.EnsureAuthenticatedPlug)
plug(Pleroma.Web.Plugs.UserIsStaffPlug)
plug(Pleroma.Web.Plugs.IdempotencyPlug)
end
pipeline :require_admin do
plug(Pleroma.Web.Plugs.UserIsAdminPlug)
end
pipeline :require_privileged_role_users_delete do
plug(:admin_api)
plug(Pleroma.Web.Plugs.EnsurePrivilegedPlug, :users_delete)
end
pipeline :require_privileged_role_users_manage_credentials do
plug(:admin_api)
plug(Pleroma.Web.Plugs.EnsurePrivilegedPlug, :users_manage_credentials)
end
pipeline :require_privileged_role_messages_read do
plug(:admin_api)
plug(Pleroma.Web.Plugs.EnsurePrivilegedPlug, :messages_read)
end
pipeline :require_privileged_role_users_manage_tags do
plug(:admin_api)
plug(Pleroma.Web.Plugs.EnsurePrivilegedPlug, :users_manage_tags)
end
pipeline :require_privileged_role_users_manage_activation_state do
plug(:admin_api)
plug(Pleroma.Web.Plugs.EnsurePrivilegedPlug, :users_manage_activation_state)
end
pipeline :require_privileged_role_users_manage_invites do
plug(:admin_api)
plug(Pleroma.Web.Plugs.EnsurePrivilegedPlug, :users_manage_invites)
end
pipeline :require_privileged_role_reports_manage_reports do
plug(:admin_api)
plug(Pleroma.Web.Plugs.EnsurePrivilegedPlug, :reports_manage_reports)
end
pipeline :require_privileged_role_users_read do
plug(:admin_api)
plug(Pleroma.Web.Plugs.EnsurePrivilegedPlug, :users_read)
end
pipeline :require_privileged_role_messages_delete do
plug(:admin_api)
plug(Pleroma.Web.Plugs.EnsurePrivilegedPlug, :messages_delete)
end
pipeline :require_privileged_role_emoji_manage_emoji do
plug(:admin_api)
plug(Pleroma.Web.Plugs.EnsurePrivilegedPlug, :emoji_manage_emoji)
end
pipeline :require_privileged_role_instances_delete do
plug(:admin_api)
plug(Pleroma.Web.Plugs.EnsurePrivilegedPlug, :instances_delete)
end
pipeline :require_privileged_role_moderation_log_read do
plug(:admin_api)
plug(Pleroma.Web.Plugs.EnsurePrivilegedPlug, :moderation_log_read)
end
pipeline :require_privileged_role_statistics_read do
plug(:admin_api)
plug(Pleroma.Web.Plugs.EnsurePrivilegedPlug, :statistics_read)
end
pipeline :require_privileged_role_announcements_manage_announcements do
plug(:admin_api)
plug(Pleroma.Web.Plugs.EnsurePrivilegedPlug, :announcements_manage_announcements)
end
pipeline :pleroma_html do
plug(:browser)
plug(:authenticate)
plug(Pleroma.Web.Plugs.EnsureUserTokenAssignsPlug)
end
pipeline :well_known do
plug(:accepts, ["json", "jrd", "jrd+json", "xml", "xrd+xml"])
end
pipeline :config do
plug(:accepts, ["json", "xml"])
plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)
end
pipeline :pleroma_api do
plug(:accepts, ["html", "json"])
plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)
end
pipeline :mailbox_preview do
plug(:accepts, ["html"])
plug(:put_secure_browser_headers, %{
"content-security-policy" =>
"default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline' 'unsafe-eval'"
})
end
pipeline :http_signature do
plug(Pleroma.Web.Plugs.HTTPSignaturePlug)
plug(Pleroma.Web.Plugs.MappedSignatureToIdentityPlug)
end
pipeline :static_fe do
plug(Pleroma.Web.Plugs.StaticFEPlug)
end
scope "/api/v1/pleroma", Pleroma.Web.TwitterAPI do
pipe_through(:pleroma_api)
get("/password_reset/:token", PasswordController, :reset, as: :reset_password)
post("/password_reset", PasswordController, :do_reset, as: :reset_password)
get("/emoji", UtilController, :emoji)
get("/captcha", UtilController, :captcha)
get("/healthcheck", UtilController, :healthcheck)
post("/remote_interaction", UtilController, :remote_interaction)
end
scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do
pipe_through(:pleroma_api)
get("/federation_status", InstancesController, :show)
end
scope "/api/v1/pleroma", Pleroma.Web do
pipe_through(:pleroma_api)
post("/uploader_callback/:upload_path", UploaderController, :callback)
end
# AdminAPI: only admins can perform these actions
scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through([:admin_api, :require_admin])
get("/users/:nickname/permission_group", AdminAPIController, :right_get)
get("/users/:nickname/permission_group/:permission_group", AdminAPIController, :right_get)
post("/users/:nickname/permission_group/:permission_group", AdminAPIController, :right_add)
delete(
"/users/:nickname/permission_group/:permission_group",
AdminAPIController,
:right_delete
)
post("/users/permission_group/:permission_group", AdminAPIController, :right_add_multiple)
delete(
"/users/permission_group/:permission_group",
AdminAPIController,
:right_delete_multiple
)
post("/users/follow", UserController, :follow)
post("/users/unfollow", UserController, :unfollow)
post("/users", UserController, :create)
patch("/users/suggest", UserController, :suggest)
patch("/users/unsuggest", UserController, :unsuggest)
get("/relay", RelayController, :index)
post("/relay", RelayController, :follow)
delete("/relay", RelayController, :unfollow)
get("/instance_document/:name", InstanceDocumentController, :show)
patch("/instance_document/:name", InstanceDocumentController, :update)
delete("/instance_document/:name", InstanceDocumentController, :delete)
get("/config", ConfigController, :show)
post("/config", ConfigController, :update)
get("/config/descriptions", ConfigController, :descriptions)
get("/need_reboot", AdminAPIController, :need_reboot)
get("/restart", AdminAPIController, :restart)
get("/oauth_app", OAuthAppController, :index)
post("/oauth_app", OAuthAppController, :create)
patch("/oauth_app/:id", OAuthAppController, :update)
delete("/oauth_app/:id", OAuthAppController, :delete)
get("/media_proxy_caches", MediaProxyCacheController, :index)
post("/media_proxy_caches/delete", MediaProxyCacheController, :delete)
post("/media_proxy_caches/purge", MediaProxyCacheController, :purge)
get("/frontends", FrontendController, :index)
post("/frontends/install", FrontendController, :install)
post("/backups", AdminAPIController, :create_backup)
end
# AdminAPI: admins and mods (staff) can perform these actions (if privileged by role)
scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through(:require_privileged_role_announcements_manage_announcements)
get("/announcements", AnnouncementController, :index)
post("/announcements", AnnouncementController, :create)
get("/announcements/:id", AnnouncementController, :show)
patch("/announcements/:id", AnnouncementController, :change)
delete("/announcements/:id", AnnouncementController, :delete)
end
# AdminAPI: admins and mods (staff) can perform these actions (if privileged by role)
scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through(:require_privileged_role_users_delete)
delete("/users", UserController, :delete)
end
# AdminAPI: admins and mods (staff) can perform these actions (if privileged by role)
scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through(:require_privileged_role_users_manage_credentials)
get("/users/:nickname/password_reset", AdminAPIController, :get_password_reset)
get("/users/:nickname/credentials", AdminAPIController, :show_user_credentials)
patch("/users/:nickname/credentials", AdminAPIController, :update_user_credentials)
put("/users/disable_mfa", AdminAPIController, :disable_mfa)
patch("/users/force_password_reset", AdminAPIController, :force_password_reset)
patch("/users/confirm_email", AdminAPIController, :confirm_email)
patch("/users/resend_confirmation_email", AdminAPIController, :resend_confirmation_email)
end
# AdminAPI: admins and mods (staff) can perform these actions (if privileged by role)
scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through(:require_privileged_role_messages_read)
get("/users/:nickname/statuses", AdminAPIController, :list_user_statuses)
get("/users/:nickname/chats", AdminAPIController, :list_user_chats)
get("/statuses", StatusController, :index)
get("/chats/:id", ChatController, :show)
get("/chats/:id/messages", ChatController, :messages)
get("/instances/:instance/statuses", InstanceController, :list_statuses)
get("/statuses/:id", StatusController, :show)
end
# AdminAPI: admins and mods (staff) can perform these actions (if privileged by role)
scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through(:require_privileged_role_users_manage_tags)
put("/users/tag", AdminAPIController, :tag_users)
delete("/users/tag", AdminAPIController, :untag_users)
end
# AdminAPI: admins and mods (staff) can perform these actions (if privileged by role)
scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through(:require_privileged_role_users_manage_activation_state)
patch("/users/:nickname/toggle_activation", UserController, :toggle_activation)
patch("/users/activate", UserController, :activate)
patch("/users/deactivate", UserController, :deactivate)
end
# AdminAPI: admins and mods (staff) can perform these actions (if privileged by role)
scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through(:require_privileged_role_users_manage_invites)
patch("/users/approve", UserController, :approve)
post("/users/invite_token", InviteController, :create)
get("/users/invites", InviteController, :index)
post("/users/revoke_invite", InviteController, :revoke)
post("/users/email_invite", InviteController, :email)
end
# AdminAPI: admins and mods (staff) can perform these actions (if privileged by role)
scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through(:require_privileged_role_reports_manage_reports)
get("/reports", ReportController, :index)
get("/reports/:id", ReportController, :show)
patch("/reports", ReportController, :update)
post("/reports/:id/notes", ReportController, :notes_create)
delete("/reports/:report_id/notes/:id", ReportController, :notes_delete)
end
# AdminAPI: admins and mods (staff) can perform these actions (if privileged by role)
scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through(:require_privileged_role_users_read)
get("/users", UserController, :index)
get("/users/:nickname", UserController, :show)
end
# AdminAPI: admins and mods (staff) can perform these actions (if privileged by role)
scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through(:require_privileged_role_messages_delete)
put("/statuses/:id", StatusController, :update)
delete("/statuses/:id", StatusController, :delete)
delete("/chats/:id/messages/:message_id", ChatController, :delete_message)
end
# AdminAPI: admins and mods (staff) can perform these actions (if privileged by role)
scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through(:require_privileged_role_emoji_manage_emoji)
post("/reload_emoji", AdminAPIController, :reload_emoji)
end
# AdminAPI: admins and mods (staff) can perform these actions (if privileged by role)
scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through(:require_privileged_role_instances_delete)
delete("/instances/:instance", InstanceController, :delete)
end
# AdminAPI: admins and mods (staff) can perform these actions (if privileged by role)
scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through(:require_privileged_role_moderation_log_read)
get("/moderation_log", AdminAPIController, :list_log)
end
# AdminAPI: admins and mods (staff) can perform these actions (if privileged by role)
scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through(:require_privileged_role_statistics_read)
get("/stats", AdminAPIController, :stats)
end
scope "/api/v1/pleroma/emoji", Pleroma.Web.PleromaAPI do
scope "/pack" do
pipe_through(:require_privileged_role_emoji_manage_emoji)
post("/", EmojiPackController, :create)
patch("/", EmojiPackController, :update)
delete("/", EmojiPackController, :delete)
end
scope "/pack" do
pipe_through(:api)
get("/", EmojiPackController, :show)
end
# Modifying packs
scope "/packs" do
pipe_through(:require_privileged_role_emoji_manage_emoji)
get("/import", EmojiPackController, :import_from_filesystem)
get("/remote", EmojiPackController, :remote)
post("/download", EmojiPackController, :download)
post("/files", EmojiFileController, :create)
patch("/files", EmojiFileController, :update)
delete("/files", EmojiFileController, :delete)
end
# Pack info / downloading
scope "/packs" do
pipe_through(:api)
get("/", EmojiPackController, :index)
get("/archive", EmojiPackController, :archive)
end
end
scope "/", Pleroma.Web.TwitterAPI do
pipe_through(:pleroma_html)
post("/main/ostatus", UtilController, :remote_subscribe)
get("/main/ostatus", UtilController, :show_subscribe_form)
get("/ostatus_subscribe", RemoteFollowController, :follow)
post("/ostatus_subscribe", RemoteFollowController, :do_follow)
get("/authorize_interaction", RemoteFollowController, :authorize_interaction)
end
scope "/api/pleroma", Pleroma.Web.TwitterAPI do
pipe_through(:authenticated_api)
post("/change_email", UtilController, :change_email)
post("/change_password", UtilController, :change_password)
post("/delete_account", UtilController, :delete_account)
put("/notification_settings", UtilController, :update_notification_settings)
post("/disable_account", UtilController, :disable_account)
post("/move_account", UtilController, :move_account)
put("/aliases", UtilController, :add_alias)
get("/aliases", UtilController, :list_aliases)
delete("/aliases", UtilController, :delete_alias)
end
scope "/api/pleroma", Pleroma.Web.PleromaAPI do
pipe_through(:authenticated_api)
post("/mutes_import", UserImportController, :mutes)
post("/blocks_import", UserImportController, :blocks)
post("/follow_import", UserImportController, :follow)
get("/accounts/mfa", TwoFactorAuthenticationController, :settings)
get("/accounts/mfa/backup_codes", TwoFactorAuthenticationController, :backup_codes)
get("/accounts/mfa/setup/:method", TwoFactorAuthenticationController, :setup)
post("/accounts/mfa/confirm/:method", TwoFactorAuthenticationController, :confirm)
delete("/accounts/mfa/:method", TwoFactorAuthenticationController, :disable)
end
scope "/oauth", Pleroma.Web.OAuth do
# Note: use /api/v1/accounts/verify_credentials for userinfo of signed-in user
get("/registration_details", OAuthController, :registration_details)
post("/mfa/verify", MFAController, :verify, as: :mfa_verify)
get("/mfa", MFAController, :show)
scope [] do
pipe_through(:oauth)
get("/authorize", OAuthController, :authorize)
post("/authorize", OAuthController, :create_authorization)
end
scope [] do
pipe_through(:fetch_session)
post("/token", OAuthController, :token_exchange)
post("/revoke", OAuthController, :token_revoke)
post("/mfa/challenge", MFAController, :challenge)
end
scope [] do
pipe_through(:browser)
get("/prepare_request", OAuthController, :prepare_request)
get("/:provider", OAuthController, :request)
get("/:provider/callback", OAuthController, :callback)
post("/register", OAuthController, :register)
end
end
scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do
pipe_through(:api)
get("/apps", AppController, :index)
get("/statuses/:id/reactions/:emoji", EmojiReactionController, :index)
get("/statuses/:id/reactions", EmojiReactionController, :index)
end
scope "/api/v0/pleroma", Pleroma.Web.PleromaAPI do
pipe_through(:authenticated_api)
get("/reports", ReportController, :index)
get("/reports/:id", ReportController, :show)
end
scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do
scope [] do
pipe_through(:authenticated_api)
post("/chats/by-account-id/:id", ChatController, :create)
get("/chats", ChatController, :index)
get("/chats/:id", ChatController, :show)
get("/chats/:id/messages", ChatController, :messages)
post("/chats/:id/messages", ChatController, :post_chat_message)
delete("/chats/:id/messages/:message_id", ChatController, :delete_message)
post("/chats/:id/read", ChatController, :mark_as_read)
post("/chats/:id/messages/:message_id/read", ChatController, :mark_message_as_read)
get("/conversations/:id/statuses", ConversationController, :statuses)
get("/conversations/:id", ConversationController, :show)
post("/conversations/read", ConversationController, :mark_as_read)
patch("/conversations/:id", ConversationController, :update)
put("/statuses/:id/reactions/:emoji", EmojiReactionController, :create)
delete("/statuses/:id/reactions/:emoji", EmojiReactionController, :delete)
post("/notifications/read", NotificationController, :mark_as_read)
get("/mascot", MascotController, :show)
put("/mascot", MascotController, :update)
post("/scrobble", ScrobbleController, :create)
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
pipe_through(:api)
get("/accounts/:id/favourites", AccountController, :favourites)
get("/accounts/:id/endorsements", AccountController, :endorsements)
get("/statuses/:id/quotes", StatusController, :quotes)
end
scope [] do
pipe_through(:authenticated_api)
post("/accounts/:id/subscribe", AccountController, :subscribe)
post("/accounts/:id/unsubscribe", AccountController, :unsubscribe)
get("/birthdays", AccountController, :birthdays)
end
scope [] do
pipe_through(:authenticated_api)
get("/settings/:app", SettingsController, :show)
patch("/settings/:app", SettingsController, :update)
end
post("/accounts/confirmation_resend", AccountController, :confirmation_resend)
end
scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do
pipe_through(:api)
get("/accounts/:id/scrobbles", ScrobbleController, :index)
end
scope "/api/v2/pleroma", Pleroma.Web.PleromaAPI do
scope [] do
pipe_through(:authenticated_api)
get("/chats", ChatController, :index2)
end
end
scope "/api/v1", Pleroma.Web.MastodonAPI do
pipe_through(:authenticated_api)
get("/accounts/verify_credentials", AccountController, :verify_credentials)
patch("/accounts/update_credentials", AccountController, :update_credentials)
get("/accounts/relationships", AccountController, :relationships)
get("/accounts/:id/lists", AccountController, :lists)
get("/accounts/:id/identity_proofs", AccountController, :identity_proofs)
get("/endorsements", AccountController, :endorsements)
get("/blocks", AccountController, :blocks)
get("/mutes", AccountController, :mutes)
post("/follows", AccountController, :follow_by_uri)
post("/accounts/:id/follow", AccountController, :follow)
post("/accounts/:id/unfollow", AccountController, :unfollow)
post("/accounts/:id/block", AccountController, :block)
post("/accounts/:id/unblock", AccountController, :unblock)
post("/accounts/:id/mute", AccountController, :mute)
post("/accounts/:id/unmute", AccountController, :unmute)
post("/accounts/:id/note", AccountController, :note)
post("/accounts/:id/pin", AccountController, :endorse)
post("/accounts/:id/unpin", AccountController, :unendorse)
post("/accounts/:id/remove_from_followers", AccountController, :remove_from_followers)
get("/conversations", ConversationController, :index)
post("/conversations/:id/read", ConversationController, :mark_as_read)
delete("/conversations/:id", ConversationController, :delete)
get("/domain_blocks", DomainBlockController, :index)
post("/domain_blocks", DomainBlockController, :create)
delete("/domain_blocks", DomainBlockController, :delete)
get("/filters", FilterController, :index)
post("/filters", FilterController, :create)
get("/filters/:id", FilterController, :show)
put("/filters/:id", FilterController, :update)
delete("/filters/:id", FilterController, :delete)
get("/follow_requests", FollowRequestController, :index)
post("/follow_requests/:id/authorize", FollowRequestController, :authorize)
post("/follow_requests/:id/reject", FollowRequestController, :reject)
get("/lists", ListController, :index)
get("/lists/:id", ListController, :show)
get("/lists/:id/accounts", ListController, :list_accounts)
delete("/lists/:id", ListController, :delete)
post("/lists", ListController, :create)
put("/lists/:id", ListController, :update)
post("/lists/:id/accounts", ListController, :add_to_list)
delete("/lists/:id/accounts", ListController, :remove_from_list)
get("/markers", MarkerController, :index)
post("/markers", MarkerController, :upsert)
post("/media", MediaController, :create)
get("/media/:id", MediaController, :show)
put("/media/:id", MediaController, :update)
get("/notifications", NotificationController, :index)
get("/notifications/:id", NotificationController, :show)
post("/notifications/:id/dismiss", NotificationController, :dismiss)
post("/notifications/clear", NotificationController, :clear)
delete("/notifications/destroy_multiple", NotificationController, :destroy_multiple)
# Deprecated: was removed in Mastodon v3, use `/notifications/:id/dismiss` instead
post("/notifications/dismiss", NotificationController, :dismiss_via_body)
post("/polls/:id/votes", PollController, :vote)
post("/reports", ReportController, :create)
get("/scheduled_statuses", ScheduledActivityController, :index)
get("/scheduled_statuses/:id", ScheduledActivityController, :show)
put("/scheduled_statuses/:id", ScheduledActivityController, :update)
delete("/scheduled_statuses/:id", ScheduledActivityController, :delete)
# Unlike `GET /api/v1/accounts/:id/favourites`, demands authentication
get("/favourites", StatusController, :favourites)
get("/bookmarks", StatusController, :bookmarks)
post("/statuses", StatusController, :create)
put("/statuses/:id", StatusController, :update)
delete("/statuses/:id", StatusController, :delete)
post("/statuses/:id/reblog", StatusController, :reblog)
post("/statuses/:id/unreblog", StatusController, :unreblog)
post("/statuses/:id/favourite", StatusController, :favourite)
post("/statuses/:id/unfavourite", StatusController, :unfavourite)
post("/statuses/:id/pin", StatusController, :pin)
post("/statuses/:id/unpin", StatusController, :unpin)
post("/statuses/:id/bookmark", StatusController, :bookmark)
post("/statuses/:id/unbookmark", StatusController, :unbookmark)
post("/statuses/:id/mute", StatusController, :mute_conversation)
post("/statuses/:id/unmute", StatusController, :unmute_conversation)
post("/push/subscription", SubscriptionController, :create)
get("/push/subscription", SubscriptionController, :show)
put("/push/subscription", SubscriptionController, :update)
delete("/push/subscription", SubscriptionController, :delete)
get("/suggestions", SuggestionController, :index)
delete("/suggestions/:account_id", SuggestionController, :dismiss)
get("/timelines/home", TimelineController, :home)
get("/timelines/direct", TimelineController, :direct)
get("/timelines/list/:list_id", TimelineController, :list)
get("/announcements", AnnouncementController, :index)
post("/announcements/:id/dismiss", AnnouncementController, :mark_read)
end
scope "/api/v1", Pleroma.Web.MastodonAPI do
pipe_through(:app_api)
post("/apps", AppController, :create)
get("/apps/verify_credentials", AppController, :verify_credentials)
end
scope "/api/v1", Pleroma.Web.MastodonAPI do
pipe_through(:api)
get("/accounts/search", SearchController, :account_search)
get("/search", SearchController, :search)
get("/accounts/lookup", AccountController, :lookup)
get("/accounts/:id/statuses", AccountController, :statuses)
get("/accounts/:id/followers", AccountController, :followers)
get("/accounts/:id/following", AccountController, :following)
get("/accounts/:id", AccountController, :show)
post("/accounts", AccountController, :create)
get("/instance", InstanceController, :show)
get("/instance/peers", InstanceController, :peers)
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)
get("/statuses/:id/source", StatusController, :show_source)
get("/custom_emojis", CustomEmojiController, :index)
get("/trends", MastodonAPIController, :empty_array)
get("/timelines/public", TimelineController, :public)
get("/timelines/tag/:tag", TimelineController, :hashtag)
get("/polls/:id", PollController, :show)
get("/directory", DirectoryController, :index)
end
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
pipe_through(:config)
get("/pleroma/frontend_configurations", TwitterAPI.UtilController, :frontend_configurations)
end
scope "/api", Pleroma.Web do
pipe_through(:api)
get(
"/account/confirm_email/:user_id/:token",
TwitterAPI.Controller,
:confirm_email,
as: :confirm_email
)
end
scope "/api" do
pipe_through(:base_api)
get("/openapi", OpenApiSpex.Plug.RenderSpec, [])
end
scope "/api", Pleroma.Web, as: :authenticated_twitter_api do
pipe_through(:authenticated_api)
get("/oauth_tokens", TwitterAPI.Controller, :oauth_tokens)
delete("/oauth_tokens/:id", TwitterAPI.Controller, :revoke_token)
end
scope "/", Pleroma.Web do
# Note: html format is supported only if static FE is enabled
# Note: http signature is only considered for json requests (no auth for non-json requests)
pipe_through([:accepts_html_json, :http_signature, :static_fe])
get("/objects/:uuid", OStatus.OStatusController, :object)
get("/activities/:uuid", OStatus.OStatusController, :activity)
get("/notice/:id", OStatus.OStatusController, :notice)
# Mastodon compatibility routes
get("/users/:nickname/statuses/:id", OStatus.OStatusController, :object)
get("/users/:nickname/statuses/:id/activity", OStatus.OStatusController, :activity)
end
scope "/", Pleroma.Web do
# Note: html format is supported only if static FE is enabled
# Note: http signature is only considered for json requests (no auth for non-json requests)
pipe_through([:accepts_html_xml_json, :http_signature, :static_fe])
# Note: returns user _profile_ for json requests, redirects to user _feed_ for non-json ones
get("/users/:nickname", Feed.UserController, :feed_redirect, as: :user_feed)
end
scope "/", Pleroma.Web do
pipe_through([:accepts_html_xml])
get("/users/:nickname/feed", Feed.UserController, :feed, as: :user_feed)
end
scope "/", Pleroma.Web do
pipe_through(:accepts_html)
get("/notice/:id/embed_player", OStatus.OStatusController, :notice_player)
end
scope "/", Pleroma.Web do
pipe_through(:accepts_xml_rss_atom)
get("/tags/:tag", Feed.TagController, :feed, as: :tag_feed)
end
scope "/", Pleroma.Web do
pipe_through(:browser)
get("/mailer/unsubscribe/:token", Mailer.SubscriptionController, :unsubscribe)
end
pipeline :ap_service_actor do
plug(:accepts, ["activity+json", "json"])
end
# Server to Server (S2S) AP interactions
pipeline :activitypub do
plug(:ap_service_actor)
plug(:http_signature)
end
# Client to Server (C2S) AP interactions
pipeline :activitypub_client do
plug(:ap_service_actor)
plug(:fetch_session)
plug(:authenticate)
plug(:after_auth)
end
scope "/", Pleroma.Web.ActivityPub do
pipe_through([:activitypub_client])
get("/api/ap/whoami", ActivityPubController, :whoami)
get("/users/:nickname/inbox", ActivityPubController, :read_inbox)
get("/users/:nickname/outbox", ActivityPubController, :outbox)
post("/users/:nickname/outbox", ActivityPubController, :update_outbox)
post("/api/ap/upload_media", ActivityPubController, :upload_media)
# The following two are S2S as well, see `ActivityPub.fetch_follow_information_for_user/1`:
get("/users/:nickname/followers", ActivityPubController, :followers)
get("/users/:nickname/following", ActivityPubController, :following)
get("/users/:nickname/collections/featured", ActivityPubController, :pinned)
end
scope "/", Pleroma.Web.ActivityPub do
pipe_through(:activitypub)
post("/inbox", ActivityPubController, :inbox)
post("/users/:nickname/inbox", ActivityPubController, :inbox)
end
scope "/relay", Pleroma.Web.ActivityPub do
pipe_through(:ap_service_actor)
get("/", ActivityPubController, :relay)
scope [] do
pipe_through(:http_signature)
post("/inbox", ActivityPubController, :inbox)
end
get("/following", ActivityPubController, :relay_following)
get("/followers", ActivityPubController, :relay_followers)
end
scope "/internal/fetch", Pleroma.Web.ActivityPub do
pipe_through(:ap_service_actor)
get("/", ActivityPubController, :internal_fetch)
post("/inbox", ActivityPubController, :inbox)
end
scope "/.well-known", Pleroma.Web do
pipe_through(:well_known)
get("/host-meta", WebFinger.WebFingerController, :host_meta)
get("/webfinger", WebFinger.WebFingerController, :webfinger)
get("/nodeinfo", Nodeinfo.NodeinfoController, :schemas)
end
scope "/nodeinfo", Pleroma.Web do
get("/:version", Nodeinfo.NodeinfoController, :nodeinfo)
end
scope "/", Pleroma.Web do
pipe_through(:api)
get("/manifest.json", ManifestController, :show)
end
scope "/", Pleroma.Web do
pipe_through(:pleroma_html)
post("/auth/password", TwitterAPI.PasswordController, :request)
end
scope "/proxy/", Pleroma.Web do
get("/preview/:sig/:url", MediaProxy.MediaProxyController, :preview)
get("/preview/:sig/:url/:filename", MediaProxy.MediaProxyController, :preview)
get("/:sig/:url", MediaProxy.MediaProxyController, :remote)
get("/:sig/:url/:filename", MediaProxy.MediaProxyController, :remote)
end
if Pleroma.Config.get(:env) == :dev do
scope "/dev" do
pipe_through([:mailbox_preview])
forward("/mailbox", Plug.Swoosh.MailboxPreview, base_path: "/dev/mailbox")
end
end
scope "/" do
pipe_through([:pleroma_html, :authenticate, :require_admin])
live_dashboard("/phoenix/live_dashboard")
end
# Test-only routes needed to test action dispatching and plug chain execution
if Pleroma.Config.get(:env) == :test do
@test_actions [
:do_oauth_check,
:fallback_oauth_check,
:skip_oauth_check,
:fallback_oauth_skip_publicity_check,
:skip_oauth_skip_publicity_check,
:missing_oauth_check_definition
]
scope "/test/api", Pleroma.Tests do
pipe_through(:api)
for action <- @test_actions do
get("/#{action}", AuthTestController, action)
end
end
scope "/test/authenticated_api", Pleroma.Tests do
pipe_through(:authenticated_api)
for action <- @test_actions do
get("/#{action}", AuthTestController, action)
end
end
end
scope "/", Pleroma.Web.MongooseIM do
get("/user_exists", MongooseIMController, :user_exists)
get("/check_password", MongooseIMController, :check_password)
end
scope "/", Pleroma.Web.Fallback do
get("/registration/:token", RedirectController, :registration_page)
get("/:maybe_nickname_or_id", RedirectController, :redirector_with_meta)
match(:*, "/api/pleroma/*path", LegacyPleromaApiRerouterPlug, [])
get("/api/*path", RedirectController, :api_not_implemented)
get("/*path", RedirectController, :redirector_with_preload)
options("/*path", RedirectController, :empty)
end
def get_api_routes do
Phoenix.Router.routes(__MODULE__)
|> Enum.reject(fn r -> r.plug == Pleroma.Web.Fallback.RedirectController end)
|> Enum.map(fn r ->
r.path
|> String.split("/", trim: true)
|> List.first()
end)
|> Enum.uniq()
end
end
diff --git a/lib/pleroma/workers/rich_media_expiration_worker.ex b/lib/pleroma/workers/rich_media_expiration_worker.ex
new file mode 100644
index 000000000..d7ae497a7
--- /dev/null
+++ 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/priv/repo/migrations/20240207035927_create_rich_media_card.exs b/priv/repo/migrations/20240207035927_create_rich_media_card.exs
new file mode 100644
index 000000000..b5e48bccb
--- /dev/null
+++ 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/test/fixtures/rich_media/reddit.html b/test/fixtures/rich_media/reddit.html
new file mode 100644
index 000000000..a99bb6884
--- /dev/null
+++ 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">&nbsp;|&nbsp;</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 &raquo;</a></div></div><div id="header-bottom-left"><a href="/" id="header-img" class="default-header" title="">reddit.com</a>&nbsp;<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?&#32;<a href="https://www.reddit.com/login" class="login-required login-link" >Log in</a>&#32;or&#32;<a href="https://www.reddit.com/login" class="login-required" >sign up</a>&#32;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 &quot;subreddit&quot;</dd><dt>author:<i>username</i></dt><dd>find submissions by &quot;username&quot;</dd><dt>site:<i>example.com</i></dt><dd>find submissions from &quot;example.com&quot;</dd><dt>url:<i>text</i></dt><dd>search for &quot;text&quot; in url</dd><dt>selftext:<i>text</i></dt><dd>search for &quot;text&quot; 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.&#32;<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 &#32;</span><time datetime="2023-09-20T07:34:36+00:00">20 Sep 2023</time></div><div class="score"><span class="number">241</span>&#32;<span class="word">points</span>&#32;(92% upvoted)</div><div class="shortlink">shortlink: &#32;<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>&#32;<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>&#32;<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 &#39;old&#39; Reddit is no longer kept up to date. Please use &#39;new&#39; Reddit. </p>
+
+<ol>
+<li><p><strong>No Low Effort / Poor Quality Posts</strong>
+&quot;This security forum is oriented towards private white hat security professionals.&quot; If a post has very basic information, it is not appropriate for this sub. For example, &quot;why passwords are important&quot; 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&#39;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&#39;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&#32;<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&#32;<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">&lt;</span><span class="read-next-button next">&gt;</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>&#32; &middot;&#32;<span class="comments">45 comments</span>&#32;</div><div class="read-next-title">What are some &quot;must-have&quot; 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>&#32; &middot;&#32;<span class="comments">27 comments</span>&#32;</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>&#32; &middot;&#32;<span class="comments">17 comments</span>&#32;</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>&#32; &middot;&#32;<span class="comments">57 comments</span>&#32;</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>&#32; &middot;&#32;<span class="comments">8 comments</span>&#32;</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>&#32; &middot;&#32;<span class="comments">296 comments</span>&#32;</div><div class="read-next-title">What is a security feature that is really &quot;security theater&quot;?</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>&#32; &middot;&#32;<span class="comments">17 comments</span>&#32;</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>&#32; &middot;&#32;<span class="comments">9 comments</span>&#32;</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>&#32; &middot;&#32;<span class="comments">1 comment</span>&#32;</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">&times;</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&#32; 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>&#32;<span class="domain">(<a href="/r/cybersecurity/">self.cybersecurity</a>)</span></p><p class="tagline ">submitted&#32;<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>&#32;by&#32;<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&#39;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:&#32;</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&amp;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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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&#39;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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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&#39;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 &#32; 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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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, &amp; Compliance" style="background-color: #373c3f; border-color: #373c3f;"><span>Governance, Risk, &amp; Compliance</span></span><span class="userattrs"></span>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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&#39;m sure Jerry Bell is cool and all but I&#39;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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#39;shitty admin move&#39; 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 &#39;private&#39; 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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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&#39;s also helpful to follow some hashtags (which the Mastodon android app can&#39;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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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&#39;t nearly as populated as it&#39;s zealots would lead you to believe, unfortunately. Great concept, but just doesn&#39;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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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&#39;t see how it&#39;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&#39;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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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&#39;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 &#32; 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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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&#39;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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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&#39;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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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&#39;t exist.</p>
+
+<p>I don&#39;t care about who posted most recently, I want to know what&#39;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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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&#39;t think algorithms are inherently bad as long as they can&#39;t be manipulated.</p>
+
+<p>That being said, Reddit mostly managed without algorithms thanks to voting &amp; community driven recommendations, but Mastadon doesn&#39;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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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&#39;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&#39;t see ads on almost any site with this method. At work I get ads all the time and it&#39;s annoying. Not sure how y&#39;all lived like this for so long. Ads haven&#39;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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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&#39;m hoping Bluesky gets more popular once it actually opens. I was able to join yesterday and there&#39;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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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&#39;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&#39;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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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&#39;m like a lot of people who have tried mastodon and nope out. </p>
+
+<p>I get the idea, but I still don&#39;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&#39;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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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&#39;m the same way. I was pretty pumped to have another platform, I&#39;m pretty over this website and I&#39;ve never been into twitter. It&#39;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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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&#39;t know why I do anymore, it&#39;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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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&#39;s an interesting one. I&#39;ll have a look as I&#39;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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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&#39;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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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&#39;t the /sec/ general completely dead or at the very least only filled with &quot;How to become 1337 haxxor&quot;?</p>
+
+<p>It&#39;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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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&amp;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&amp;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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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&#39;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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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 &#32; 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>&#32;<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>&#32;<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>&nbsp;<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&#32;are&#32;automatically&#32;archived&#32;after&#32;6&#32;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&nbsp;<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>&nbsp;or&nbsp;<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&#32;are&#32;automatically&#32;archived&#32;after&#32;6&#32;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&nbsp;<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>&nbsp;or&nbsp;<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 &amp; 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">&lt;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&#32;<a href="https://www.reddit.com/help/useragreement" >User Agreement</a>&#32;and&#32;<a href="https://www.reddit.com/help/privacypolicy" >Privacy Policy</a>. &copy; 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>&nbsp|<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&#32;<a href="https://www.reddit.com/help/useragreement/" >Terms</a>&#32;and that you have read our&#32;<a href="https://www.reddit.com/help/privacypolicy/" >Privacy Policy</a>&#32;and&#32;<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>&nbsp;<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>&#32;<span class="details hover">(*) incomplete &#32;<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">&pi;</span>&nbsp;<span class="content">Rendered by PID 29 on&#32; reddit-service-r2-slowlane-65c5c76ff5-v258h &#32;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/pleroma/html_test.exs b/test/pleroma/html_test.exs
index b99689903..1be161971 100644
--- a/test/pleroma/html_test.exs
+++ b/test/pleroma/html_test.exs
@@ -1,281 +1,281 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.HTMLTest do
alias Pleroma.HTML
alias Pleroma.Object
alias Pleroma.Web.CommonAPI
use Pleroma.DataCase, async: true
import Pleroma.Factory
@html_sample """
<b>this is in bold</b>
<p>this is a paragraph</p>
this is a linebreak<br />
this is a link with allowed "rel" attribute: <a href="http://example.com/" rel="tag">example.com</a>
this is a link with not allowed "rel" attribute: <a href="http://example.com/" rel="tag noallowed">example.com</a>
this is an image: <img src="http://example.com/image.jpg"><br />
this is an inline emoji: <img class="emoji" src="http://example.com/image.jpg"><br />
<script>alert('hacked')</script>
"""
@html_onerror_sample """
<img src="http://example.com/image.jpg" onerror="alert('hacked')">
"""
@html_stillimage_sample """
<img class="still-image" src="http://example.com/image.jpg">
"""
@html_span_class_sample """
<span class="animate-spin">hi</span>
"""
@html_span_microformats_sample """
<span class="h-card"><a class="u-url mention">@<span>foo</span></a></span>
"""
@html_span_invalid_microformats_sample """
<span class="h-card"><a class="u-url mention animate-spin">@<span>foo</span></a></span>
"""
describe "StripTags scrubber" do
test "works as expected" do
expected = """
this is in bold
this is a paragraph
this is a linebreak
this is a link with allowed &quot;rel&quot; attribute: example.com
this is a link with not allowed &quot;rel&quot; attribute: example.com
this is an image:
this is an inline emoji:
alert(&#39;hacked&#39;)
"""
assert expected == HTML.strip_tags(@html_sample)
end
test "does not allow attribute-based XSS" do
expected = "\n"
assert expected == HTML.strip_tags(@html_onerror_sample)
end
end
describe "TwitterText scrubber" do
test "normalizes HTML as expected" do
expected = """
this is in bold
<p>this is a paragraph</p>
this is a linebreak<br/>
this is a link with allowed &quot;rel&quot; attribute: <a href="http://example.com/" rel="tag">example.com</a>
this is a link with not allowed &quot;rel&quot; attribute: <a href="http://example.com/">example.com</a>
this is an image: <img src="http://example.com/image.jpg"/><br/>
this is an inline emoji: <img class="emoji" src="http://example.com/image.jpg"/><br/>
alert(&#39;hacked&#39;)
"""
assert expected == HTML.filter_tags(@html_sample, Pleroma.HTML.Scrubber.TwitterText)
end
test "does not allow attribute-based XSS" do
expected = """
<img src="http://example.com/image.jpg"/>
"""
assert expected == HTML.filter_tags(@html_onerror_sample, Pleroma.HTML.Scrubber.TwitterText)
end
test "does not allow spans with invalid classes" do
expected = """
<span>hi</span>
"""
assert expected ==
HTML.filter_tags(@html_span_class_sample, Pleroma.HTML.Scrubber.TwitterText)
end
test "does not allow images with invalid classes" do
expected = """
<img src="http://example.com/image.jpg"/>
"""
assert expected ==
HTML.filter_tags(@html_stillimage_sample, Pleroma.HTML.Scrubber.TwitterText)
end
test "does allow microformats" do
expected = """
<span class="h-card"><a class="u-url mention">@<span>foo</span></a></span>
"""
assert expected ==
HTML.filter_tags(@html_span_microformats_sample, Pleroma.HTML.Scrubber.TwitterText)
end
test "filters invalid microformats markup" do
expected = """
<span class="h-card"><a>@<span>foo</span></a></span>
"""
assert expected ==
HTML.filter_tags(
@html_span_invalid_microformats_sample,
Pleroma.HTML.Scrubber.TwitterText
)
end
end
describe "default scrubber" do
test "normalizes HTML as expected" do
expected = """
<b>this is in bold</b>
<p>this is a paragraph</p>
this is a linebreak<br/>
this is a link with allowed &quot;rel&quot; attribute: <a href="http://example.com/" rel="tag">example.com</a>
this is a link with not allowed &quot;rel&quot; attribute: <a href="http://example.com/">example.com</a>
this is an image: <img src="http://example.com/image.jpg"/><br/>
this is an inline emoji: <img class="emoji" src="http://example.com/image.jpg"/><br/>
alert(&#39;hacked&#39;)
"""
assert expected == HTML.filter_tags(@html_sample, Pleroma.HTML.Scrubber.Default)
end
test "does not allow attribute-based XSS" do
expected = """
<img src="http://example.com/image.jpg"/>
"""
assert expected == HTML.filter_tags(@html_onerror_sample, Pleroma.HTML.Scrubber.Default)
end
test "does not allow spans with invalid classes" do
expected = """
<span>hi</span>
"""
assert expected == HTML.filter_tags(@html_span_class_sample, Pleroma.HTML.Scrubber.Default)
end
test "does not allow images with invalid classes" do
expected = """
<img src="http://example.com/image.jpg"/>
"""
assert expected ==
HTML.filter_tags(@html_stillimage_sample, Pleroma.HTML.Scrubber.TwitterText)
end
test "does allow microformats" do
expected = """
<span class="h-card"><a class="u-url mention">@<span>foo</span></a></span>
"""
assert expected ==
HTML.filter_tags(@html_span_microformats_sample, Pleroma.HTML.Scrubber.Default)
end
test "filters invalid microformats markup" do
expected = """
<span class="h-card"><a>@<span>foo</span></a></span>
"""
assert expected ==
HTML.filter_tags(
@html_span_invalid_microformats_sample,
Pleroma.HTML.Scrubber.Default
)
end
end
describe "extract_first_external_url_from_object" do
test "extracts the url" do
user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
status:
"I think I just found the best github repo https://github.com/komeiji-satori/Dress"
})
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
test "skips mentions" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
status:
"@#{other_user.nickname} install misskey! https://github.com/syuilo/misskey/blob/develop/docs/setup.en.md"
})
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"
refute url == other_user.ap_id
end
test "skips hashtags" do
user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
status: "#cofe https://www.pixiv.net/member_illust.php?mode=medium&illust_id=72255140"
})
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
test "skips microformats hashtags" do
user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
status:
"<a href=\"https://pleroma.gov/tags/cofe\" rel=\"tag\">#cofe</a> https://www.pixiv.net/member_illust.php?mode=medium&illust_id=72255140",
content_type: "text/html"
})
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
test "does not crash when there is an HTML entity in a link" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "\"http://cofe.com/?boomer=ok&foo=bar\""})
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
user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
status:
"<a href=\"https://pleroma.gov/media/d24caa3a498e21e0298377a9ca0149a4f4f8b767178aacf837542282e2d94fb1.png?name=image.png\" class=\"attachment\">image.png</a>"
})
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/web/mastodon_api/controllers/status_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs
index 3d8a0fa99..80c1ed099 100644
--- a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs
+++ b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs
@@ -1,2607 +1,2466 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
use Pleroma.Web.ConnCase, async: false
use Oban.Testing, repo: Pleroma.Repo
alias Pleroma.Activity
alias Pleroma.Conversation.Participation
alias Pleroma.ModerationLog
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
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.CommonAPI
alias Pleroma.Workers.ScheduledActivityWorker
import Mox
import Pleroma.Factory
setup do: clear_config([:instance, :federating])
setup do: clear_config([:instance, :allow_relay])
setup do: clear_config([:mrf, :policies])
setup do: clear_config([:mrf_keyword, :reject])
setup do
Pleroma.UnstubbedConfigMock
|> stub_with(Pleroma.Config)
Pleroma.StaticStubbedConfigMock
|> stub(:get, fn
[:rich_media, :enabled] -> false
path -> Pleroma.Test.StaticConfig.get(path)
end)
:ok
end
describe "posting statuses" do
setup do: oauth_access(["write:statuses"])
test "posting a status does not increment reblog_count when relaying", %{conn: conn} do
clear_config([:instance, :federating], true)
clear_config([:instance, :allow_relay], true)
response =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"content_type" => "text/plain",
"source" => "Pleroma FE",
"status" => "Hello world",
"visibility" => "public"
})
|> json_response_and_validate_schema(200)
assert response["reblogs_count"] == 0
ObanHelpers.perform_all()
response =
conn
|> get("/api/v1/statuses/#{response["id"]}", %{})
|> json_response_and_validate_schema(200)
assert response["reblogs_count"] == 0
end
test "posting a status", %{conn: conn} do
idempotency_key = "Pikachu rocks!"
conn_one =
conn
|> put_req_header("content-type", "application/json")
|> put_req_header("idempotency-key", idempotency_key)
|> post("/api/v1/statuses", %{
"status" => "cofe",
"spoiler_text" => "2hu",
"sensitive" => "0"
})
assert %{"content" => "cofe", "id" => id, "spoiler_text" => "2hu", "sensitive" => false} =
json_response_and_validate_schema(conn_one, 200)
assert Activity.get_by_id(id)
conn_two =
conn
|> put_req_header("content-type", "application/json")
|> put_req_header("idempotency-key", idempotency_key)
|> post("/api/v1/statuses", %{
"status" => "cofe",
"spoiler_text" => "2hu",
"sensitive" => 0
})
# Idempotency plug response means detection fail
assert %{"id" => second_id} = json_response(conn_two, 200)
assert id == second_id
conn_three =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "cofe",
"spoiler_text" => "2hu",
"sensitive" => "False"
})
assert %{"id" => third_id} = json_response_and_validate_schema(conn_three, 200)
refute id == third_id
# An activity that will expire:
# 2 hours
expires_in = 2 * 60 * 60
expires_at = DateTime.add(DateTime.utc_now(), expires_in)
conn_four =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "oolong",
"expires_in" => expires_in
})
assert %{"id" => fourth_id} = json_response_and_validate_schema(conn_four, 200)
assert Activity.get_by_id(fourth_id)
assert_enqueued(
worker: Pleroma.Workers.PurgeExpiredActivity,
args: %{activity_id: fourth_id},
scheduled_at: expires_at
)
end
test "posting a quote post", %{conn: conn} do
user = insert(:user)
{:ok, %{id: activity_id} = activity} = CommonAPI.post(user, %{status: "yolo"})
%{data: %{"id" => quote_url}} = Object.normalize(activity)
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "indeed",
"quote_id" => activity_id
})
assert %{
"id" => id,
"pleroma" => %{"quote" => %{"id" => ^activity_id}, "quote_url" => ^quote_url}
} = json_response_and_validate_schema(conn, 200)
assert Activity.get_by_id(id)
end
test "it fails to create a status if `expires_in` is less or equal than an hour", %{
conn: conn
} do
# 1 minute
expires_in = 1 * 60
assert %{"error" => "Expiry date is too soon"} =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "oolong",
"expires_in" => expires_in
})
|> json_response_and_validate_schema(422)
# 5 minutes
expires_in = 5 * 60
assert %{"error" => "Expiry date is too soon"} =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "oolong",
"expires_in" => expires_in
})
|> json_response_and_validate_schema(422)
end
test "Get MRF reason when posting a status is rejected by one", %{conn: conn} do
clear_config([:mrf_keyword, :reject], ["GNO"])
clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.KeywordPolicy])
assert %{"error" => "[KeywordPolicy] Matches with rejected keyword"} =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{"status" => "GNO/Linux"})
|> json_response_and_validate_schema(422)
end
test "posting an undefined status with an attachment", %{user: user, conn: conn} do
file = %Plug.Upload{
content_type: "image/jpeg",
path: Path.absname("test/fixtures/image.jpg"),
filename: "an_image.jpg"
}
{:ok, upload} = ActivityPub.upload(file, actor: user.ap_id)
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"media_ids" => [to_string(upload.id)]
})
assert json_response_and_validate_schema(conn, 200)
end
test "replying to a status", %{user: user, conn: conn} do
{:ok, replied_to} = CommonAPI.post(user, %{status: "cofe"})
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => replied_to.id})
assert %{"content" => "xD", "id" => id} = json_response_and_validate_schema(conn, 200)
activity = Activity.get_by_id(id)
assert activity.data["context"] == replied_to.data["context"]
assert Activity.get_in_reply_to_activity(activity).id == replied_to.id
end
test "replying to a direct message with visibility other than direct", %{
user: user,
conn: conn
} do
{:ok, replied_to} = CommonAPI.post(user, %{status: "suya..", visibility: "direct"})
Enum.each(["public", "private", "unlisted"], fn visibility ->
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "@#{user.nickname} hey",
"in_reply_to_id" => replied_to.id,
"visibility" => visibility
})
assert json_response_and_validate_schema(conn, 422) == %{
"error" => "The message visibility must be direct"
}
end)
end
test "posting a status with an invalid in_reply_to_id", %{conn: conn} do
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => ""})
assert %{"content" => "xD", "id" => id} = json_response_and_validate_schema(conn, 200)
assert Activity.get_by_id(id)
end
test "posting a sensitive status", %{conn: conn} do
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{"status" => "cofe", "sensitive" => true})
assert %{"content" => "cofe", "id" => id, "sensitive" => true} =
json_response_and_validate_schema(conn, 200)
assert Activity.get_by_id(id)
end
test "posting a fake status", %{conn: conn} do
real_conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" =>
"\"Tenshi Eating a Corndog\" is a much discussed concept on /jp/. The significance of it is disputed, so I will focus on one core concept: the symbolism behind it"
})
real_status = json_response_and_validate_schema(real_conn, 200)
assert real_status
assert Object.get_by_ap_id(real_status["uri"])
real_status =
real_status
|> Map.put("id", nil)
|> Map.put("url", nil)
|> Map.put("uri", nil)
|> Map.put("created_at", nil)
|> Kernel.put_in(["pleroma", "context"], nil)
|> Kernel.put_in(["pleroma", "conversation_id"], nil)
fake_conn =
conn
|> assign(:user, refresh_record(conn.assigns.user))
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" =>
"\"Tenshi Eating a Corndog\" is a much discussed concept on /jp/. The significance of it is disputed, so I will focus on one core concept: the symbolism behind it",
"preview" => true
})
fake_status = json_response_and_validate_schema(fake_conn, 200)
assert fake_status
refute Object.get_by_ap_id(fake_status["uri"])
fake_status =
fake_status
|> Map.put("id", nil)
|> Map.put("url", nil)
|> Map.put("uri", nil)
|> Map.put("created_at", nil)
|> Kernel.put_in(["pleroma", "context"], nil)
|> Kernel.put_in(["pleroma", "conversation_id"], nil)
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_global(fn
- 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}"
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{"status" => content, "visibility" => "direct"})
assert %{"id" => id} = response = json_response_and_validate_schema(conn, 200)
assert response["visibility"] == "direct"
assert response["pleroma"]["direct_conversation_id"]
assert activity = Activity.get_by_id(id)
assert activity.recipients == [user2.ap_id, conn.assigns[:user].ap_id]
assert activity.data["to"] == [user2.ap_id]
assert activity.data["cc"] == []
end
test "discloses application metadata when enabled" do
user = insert(:user, disclose_client: true)
%{user: _user, token: token, conn: conn} = oauth_access(["write:statuses"], user: user)
%Pleroma.Web.OAuth.Token{
app: %Pleroma.Web.OAuth.App{
client_name: app_name,
website: app_website
}
} = token
result =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "cofe is my copilot"
})
assert %{
"content" => "cofe is my copilot"
} = json_response_and_validate_schema(result, 200)
activity = result.assigns.activity.id
result =
conn
|> get("/api/v1/statuses/#{activity}")
assert %{
"content" => "cofe is my copilot",
"application" => %{
"name" => ^app_name,
"website" => ^app_website
}
} = json_response_and_validate_schema(result, 200)
end
test "hides application metadata when disabled" do
user = insert(:user, disclose_client: false)
%{user: _user, token: _token, conn: conn} = oauth_access(["write:statuses"], user: user)
result =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "club mate is my wingman"
})
assert %{"content" => "club mate is my wingman"} =
json_response_and_validate_schema(result, 200)
activity = result.assigns.activity.id
result =
conn
|> get("/api/v1/statuses/#{activity}")
assert %{
"content" => "club mate is my wingman",
"application" => nil
} = json_response_and_validate_schema(result, 200)
end
end
describe "posting scheduled statuses" do
setup do: oauth_access(["write:statuses"])
test "creates a scheduled activity", %{conn: conn} do
scheduled_at =
NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(120), :millisecond)
|> NaiveDateTime.to_iso8601()
|> Kernel.<>("Z")
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "scheduled",
"scheduled_at" => scheduled_at
})
assert %{"scheduled_at" => expected_scheduled_at} =
json_response_and_validate_schema(conn, 200)
assert expected_scheduled_at == CommonAPI.Utils.to_masto_date(scheduled_at)
assert [] == Repo.all(Activity)
end
test "with expiration" do
%{conn: conn} = oauth_access(["write:statuses", "read:statuses"])
scheduled_at =
NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(6), :millisecond)
|> NaiveDateTime.to_iso8601()
|> Kernel.<>("Z")
assert %{"id" => status_id, "params" => %{"expires_in" => 300}} =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "scheduled",
"scheduled_at" => scheduled_at,
"expires_in" => 300
})
|> json_response_and_validate_schema(200)
assert %{"id" => ^status_id, "params" => %{"expires_in" => 300}} =
conn
|> put_req_header("content-type", "application/json")
|> get("/api/v1/scheduled_statuses/#{status_id}")
|> json_response_and_validate_schema(200)
end
test "ignores nil values", %{conn: conn} do
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "not scheduled",
"scheduled_at" => nil
})
assert result = json_response_and_validate_schema(conn, 200)
assert Activity.get_by_id(result["id"])
end
test "creates a scheduled activity with a media attachment", %{user: user, conn: conn} do
scheduled_at =
NaiveDateTime.utc_now()
|> NaiveDateTime.add(:timer.minutes(120), :millisecond)
|> NaiveDateTime.to_iso8601()
|> Kernel.<>("Z")
file = %Plug.Upload{
content_type: "image/jpeg",
path: Path.absname("test/fixtures/image.jpg"),
filename: "an_image.jpg"
}
{:ok, upload} = ActivityPub.upload(file, actor: user.ap_id)
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"media_ids" => [to_string(upload.id)],
"status" => "scheduled",
"scheduled_at" => scheduled_at
})
assert %{"media_attachments" => [media_attachment]} =
json_response_and_validate_schema(conn, 200)
assert %{"type" => "image"} = media_attachment
end
test "skips the scheduling and creates the activity if scheduled_at is earlier than 5 minutes from now",
%{conn: conn} do
scheduled_at =
NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(5) - 1, :millisecond)
|> NaiveDateTime.to_iso8601()
|> Kernel.<>("Z")
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "not scheduled",
"scheduled_at" => scheduled_at
})
assert %{"content" => "not scheduled"} = json_response_and_validate_schema(conn, 200)
assert [] == Repo.all(ScheduledActivity)
end
test "returns error when daily user limit is exceeded", %{user: user, conn: conn} do
today =
NaiveDateTime.utc_now()
|> NaiveDateTime.add(:timer.minutes(6), :millisecond)
|> NaiveDateTime.to_iso8601()
# TODO
|> Kernel.<>("Z")
attrs = %{params: %{}, scheduled_at: today}
{:ok, _} = ScheduledActivity.create(user, attrs)
{:ok, _} = ScheduledActivity.create(user, attrs)
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{"status" => "scheduled", "scheduled_at" => today})
assert %{"error" => "daily limit exceeded"} == json_response_and_validate_schema(conn, 422)
end
test "returns error when total user limit is exceeded", %{user: user, conn: conn} do
today =
NaiveDateTime.utc_now()
|> NaiveDateTime.add(:timer.minutes(6), :millisecond)
|> NaiveDateTime.to_iso8601()
|> Kernel.<>("Z")
tomorrow =
NaiveDateTime.utc_now()
|> NaiveDateTime.add(:timer.hours(36), :millisecond)
|> NaiveDateTime.to_iso8601()
|> Kernel.<>("Z")
attrs = %{params: %{}, scheduled_at: today}
{:ok, _} = ScheduledActivity.create(user, attrs)
{:ok, _} = ScheduledActivity.create(user, attrs)
{:ok, _} = ScheduledActivity.create(user, %{params: %{}, scheduled_at: tomorrow})
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{"status" => "scheduled", "scheduled_at" => tomorrow})
assert %{"error" => "total limit exceeded"} == json_response_and_validate_schema(conn, 422)
end
end
describe "posting polls" do
setup do: oauth_access(["write:statuses"])
test "posting a poll", %{conn: conn} do
time = NaiveDateTime.utc_now()
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "Who is the #bestgrill?",
"poll" => %{
"options" => ["Rei", "Asuka", "Misato"],
"expires_in" => 420
}
})
response = json_response_and_validate_schema(conn, 200)
assert Enum.all?(response["poll"]["options"], fn %{"title" => title} ->
title in ["Rei", "Asuka", "Misato"]
end)
assert NaiveDateTime.diff(NaiveDateTime.from_iso8601!(response["poll"]["expires_at"]), time) in 420..430
assert response["poll"]["expired"] == false
question = Object.get_by_id(response["poll"]["id"])
# closed contains utc timezone
assert question.data["closed"] =~ "Z"
end
test "option limit is enforced", %{conn: conn} do
limit = Config.get([:instance, :poll_limits, :max_options])
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "desu~",
"poll" => %{
"options" => Enum.map(0..limit, fn num -> "desu #{num}" end),
"expires_in" => 1
}
})
%{"error" => error} = json_response_and_validate_schema(conn, 422)
assert error == "Poll can't contain more than #{limit} options"
end
test "option character limit is enforced", %{conn: conn} do
limit = Config.get([:instance, :poll_limits, :max_option_chars])
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "...",
"poll" => %{
"options" => [String.duplicate(".", limit + 1), "lol"],
"expires_in" => 1
}
})
%{"error" => error} = json_response_and_validate_schema(conn, 422)
assert error == "Poll options cannot be longer than #{limit} characters each"
end
test "minimal date limit is enforced", %{conn: conn} do
limit = Config.get([:instance, :poll_limits, :min_expiration])
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "imagine arbitrary limits",
"poll" => %{
"options" => ["this post was made by pleroma gang"],
"expires_in" => limit - 1
}
})
%{"error" => error} = json_response_and_validate_schema(conn, 422)
assert error == "Expiration date is too soon"
end
test "maximum date limit is enforced", %{conn: conn} do
limit = Config.get([:instance, :poll_limits, :max_expiration])
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "imagine arbitrary limits",
"poll" => %{
"options" => ["this post was made by pleroma gang"],
"expires_in" => limit + 1
}
})
%{"error" => error} = json_response_and_validate_schema(conn, 422)
assert error == "Expiration date is too far in the future"
end
test "scheduled poll", %{conn: conn} do
clear_config([ScheduledActivity, :enabled], true)
scheduled_at =
NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(6), :millisecond)
|> NaiveDateTime.to_iso8601()
|> Kernel.<>("Z")
%{"id" => scheduled_id} =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "very cool poll",
"poll" => %{
"options" => ~w(a b c),
"expires_in" => 420
},
"scheduled_at" => scheduled_at
})
|> json_response_and_validate_schema(200)
assert {:ok, %{id: activity_id}} =
perform_job(ScheduledActivityWorker, %{
activity_id: scheduled_id
})
refute_enqueued(worker: ScheduledActivityWorker)
object =
Activity
|> Repo.get(activity_id)
|> Object.normalize()
assert object.data["content"] == "very cool poll"
assert object.data["type"] == "Question"
assert length(object.data["oneOf"]) == 3
end
test "cannot have only one option", %{conn: conn} do
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "desu~",
"poll" => %{"options" => ["mew"], "expires_in" => 1}
})
%{"error" => error} = json_response_and_validate_schema(conn, 422)
assert error == "Poll must contain at least 2 options"
end
test "cannot have only duplicated options", %{conn: conn} do
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "desu~",
"poll" => %{"options" => ["mew", "mew"], "expires_in" => 1}
})
%{"error" => error} = json_response_and_validate_schema(conn, 422)
assert error == "Poll must contain at least 2 options"
end
end
test "get a status" do
%{conn: conn} = oauth_access(["read:statuses"])
activity = insert(:note_activity)
conn = get(conn, "/api/v1/statuses/#{activity.id}")
assert %{"id" => id} = json_response_and_validate_schema(conn, 200)
assert id == to_string(activity.id)
end
defp local_and_remote_activities do
local = insert(:note_activity)
remote = insert(:note_activity, local: false)
{:ok, local: local, remote: remote}
end
defp local_and_remote_context_activities do
local_user_1 = insert(:user)
local_user_2 = insert(:user)
remote_user = insert(:user, local: false)
{:ok, %{id: id1, data: %{"context" => context}}} =
CommonAPI.post(local_user_1, %{status: "post"})
{:ok, %{id: id2} = post} =
CommonAPI.post(local_user_2, %{status: "local reply", in_reply_to_status_id: id1})
params = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"actor" => remote_user.ap_id,
"type" => "Create",
"context" => context,
"id" => "#{remote_user.ap_id}/activities/1",
"inReplyTo" => post.data["id"],
"object" => %{
"type" => "Note",
"content" => "remote reply",
"context" => context,
"id" => "#{remote_user.ap_id}/objects/1",
"attributedTo" => remote_user.ap_id,
"to" => [
local_user_1.ap_id,
local_user_2.ap_id,
"https://www.w3.org/ns/activitystreams#Public"
]
},
"to" => [
local_user_1.ap_id,
local_user_2.ap_id,
"https://www.w3.org/ns/activitystreams#Public"
]
}
{:ok, job} = Pleroma.Web.Federator.incoming_ap_doc(params)
{:ok, remote_activity} = ObanHelpers.perform(job)
%{locals: [id1, id2], remote: remote_activity.id, context: context}
end
describe "status with restrict unauthenticated activities for local and remote" do
setup do: local_and_remote_activities()
setup do: clear_config([:restrict_unauthenticated, :activities, :local], true)
setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true)
test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
res_conn = get(conn, "/api/v1/statuses/#{local.id}")
assert json_response_and_validate_schema(res_conn, :not_found) == %{
"error" => "Record not found"
}
res_conn = get(conn, "/api/v1/statuses/#{remote.id}")
assert json_response_and_validate_schema(res_conn, :not_found) == %{
"error" => "Record not found"
}
end
test "if user is authenticated", %{local: local, remote: remote} do
%{conn: conn} = oauth_access(["read"])
res_conn = get(conn, "/api/v1/statuses/#{local.id}")
assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
res_conn = get(conn, "/api/v1/statuses/#{remote.id}")
assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
end
end
describe "status with restrict unauthenticated activities for local" do
setup do: local_and_remote_activities()
setup do: clear_config([:restrict_unauthenticated, :activities, :local], true)
test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
res_conn = get(conn, "/api/v1/statuses/#{local.id}")
assert json_response_and_validate_schema(res_conn, :not_found) == %{
"error" => "Record not found"
}
res_conn = get(conn, "/api/v1/statuses/#{remote.id}")
assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
end
test "if user is authenticated", %{local: local, remote: remote} do
%{conn: conn} = oauth_access(["read"])
res_conn = get(conn, "/api/v1/statuses/#{local.id}")
assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
res_conn = get(conn, "/api/v1/statuses/#{remote.id}")
assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
end
end
describe "status with restrict unauthenticated activities for remote" do
setup do: local_and_remote_activities()
setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true)
test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
res_conn = get(conn, "/api/v1/statuses/#{local.id}")
assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
res_conn = get(conn, "/api/v1/statuses/#{remote.id}")
assert json_response_and_validate_schema(res_conn, :not_found) == %{
"error" => "Record not found"
}
end
test "if user is authenticated", %{local: local, remote: remote} do
%{conn: conn} = oauth_access(["read"])
res_conn = get(conn, "/api/v1/statuses/#{local.id}")
assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
res_conn = get(conn, "/api/v1/statuses/#{remote.id}")
assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
end
end
test "getting a status that doesn't exist returns 404" do
%{conn: conn} = oauth_access(["read:statuses"])
activity = insert(:note_activity)
conn = get(conn, "/api/v1/statuses/#{String.downcase(activity.id)}")
assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"}
end
test "get a direct status" do
%{user: user, conn: conn} = oauth_access(["read:statuses"])
other_user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{status: "@#{other_user.nickname}", visibility: "direct"})
conn =
conn
|> assign(:user, user)
|> get("/api/v1/statuses/#{activity.id}")
[participation] = Participation.for_user(user)
res = json_response_and_validate_schema(conn, 200)
assert res["pleroma"]["direct_conversation_id"] == participation.id
end
test "get statuses by IDs" do
%{conn: conn} = oauth_access(["read:statuses"])
%{id: id1} = insert(:note_activity)
%{id: id2} = insert(:note_activity)
query_string = "ids[]=#{id1}&ids[]=#{id2}"
conn = get(conn, "/api/v1/statuses/?#{query_string}")
assert [%{"id" => ^id1}, %{"id" => ^id2}] =
Enum.sort_by(json_response_and_validate_schema(conn, :ok), & &1["id"])
end
describe "getting statuses by ids with restricted unauthenticated for local and remote" do
setup do: local_and_remote_activities()
setup do: clear_config([:restrict_unauthenticated, :activities, :local], true)
setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true)
test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}")
assert json_response_and_validate_schema(res_conn, 200) == []
end
test "if user is authenticated", %{local: local, remote: remote} do
%{conn: conn} = oauth_access(["read"])
res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}")
assert length(json_response_and_validate_schema(res_conn, 200)) == 2
end
end
describe "getting statuses by ids with restricted unauthenticated for local" do
setup do: local_and_remote_activities()
setup do: clear_config([:restrict_unauthenticated, :activities, :local], true)
test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}")
remote_id = remote.id
assert [%{"id" => ^remote_id}] = json_response_and_validate_schema(res_conn, 200)
end
test "if user is authenticated", %{local: local, remote: remote} do
%{conn: conn} = oauth_access(["read"])
res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}")
assert length(json_response_and_validate_schema(res_conn, 200)) == 2
end
end
describe "getting statuses by ids with restricted unauthenticated for remote" do
setup do: local_and_remote_activities()
setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true)
test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}")
local_id = local.id
assert [%{"id" => ^local_id}] = json_response_and_validate_schema(res_conn, 200)
end
test "if user is authenticated", %{local: local, remote: remote} do
%{conn: conn} = oauth_access(["read"])
res_conn = get(conn, "/api/v1/statuses?ids[]=#{local.id}&ids[]=#{remote.id}")
assert length(json_response_and_validate_schema(res_conn, 200)) == 2
end
end
describe "getting status contexts restricted unauthenticated for local and remote" do
setup do: local_and_remote_context_activities()
setup do: clear_config([:restrict_unauthenticated, :activities, :local], true)
setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true)
test "if user is unauthenticated", %{conn: conn, locals: [post_id, _]} do
res_conn = get(conn, "/api/v1/statuses/#{post_id}/context")
assert json_response_and_validate_schema(res_conn, 200) == %{
"ancestors" => [],
"descendants" => []
}
end
test "if user is unauthenticated reply", %{conn: conn, locals: [_, reply_id]} do
res_conn = get(conn, "/api/v1/statuses/#{reply_id}/context")
assert json_response_and_validate_schema(res_conn, 200) == %{
"ancestors" => [],
"descendants" => []
}
end
test "if user is authenticated", %{locals: [post_id, reply_id], remote: remote_reply_id} do
%{conn: conn} = oauth_access(["read"])
res_conn = get(conn, "/api/v1/statuses/#{post_id}/context")
%{"ancestors" => [], "descendants" => descendants} =
json_response_and_validate_schema(res_conn, 200)
descendant_ids =
descendants
|> Enum.map(& &1["id"])
assert reply_id in descendant_ids
assert remote_reply_id in descendant_ids
end
test "if user is authenticated reply", %{locals: [post_id, reply_id], remote: remote_reply_id} do
%{conn: conn} = oauth_access(["read"])
res_conn = get(conn, "/api/v1/statuses/#{reply_id}/context")
%{"ancestors" => ancestors, "descendants" => descendants} =
json_response_and_validate_schema(res_conn, 200)
ancestor_ids =
ancestors
|> Enum.map(& &1["id"])
descendant_ids =
descendants
|> Enum.map(& &1["id"])
assert post_id in ancestor_ids
assert remote_reply_id in descendant_ids
end
end
describe "getting status contexts restricted unauthenticated for local" do
setup do: local_and_remote_context_activities()
setup do: clear_config([:restrict_unauthenticated, :activities, :local], true)
setup do: clear_config([:restrict_unauthenticated, :activities, :remote], false)
test "if user is unauthenticated", %{
conn: conn,
locals: [post_id, reply_id],
remote: remote_reply_id
} do
res_conn = get(conn, "/api/v1/statuses/#{post_id}/context")
%{"ancestors" => [], "descendants" => descendants} =
json_response_and_validate_schema(res_conn, 200)
descendant_ids =
descendants
|> Enum.map(& &1["id"])
assert reply_id not in descendant_ids
assert remote_reply_id in descendant_ids
end
test "if user is unauthenticated reply", %{
conn: conn,
locals: [post_id, reply_id],
remote: remote_reply_id
} do
res_conn = get(conn, "/api/v1/statuses/#{reply_id}/context")
%{"ancestors" => ancestors, "descendants" => descendants} =
json_response_and_validate_schema(res_conn, 200)
ancestor_ids =
ancestors
|> Enum.map(& &1["id"])
descendant_ids =
descendants
|> Enum.map(& &1["id"])
assert post_id not in ancestor_ids
assert remote_reply_id in descendant_ids
end
test "if user is authenticated", %{locals: [post_id, reply_id], remote: remote_reply_id} do
%{conn: conn} = oauth_access(["read"])
res_conn = get(conn, "/api/v1/statuses/#{post_id}/context")
%{"ancestors" => [], "descendants" => descendants} =
json_response_and_validate_schema(res_conn, 200)
descendant_ids =
descendants
|> Enum.map(& &1["id"])
assert reply_id in descendant_ids
assert remote_reply_id in descendant_ids
end
test "if user is authenticated reply", %{locals: [post_id, reply_id], remote: remote_reply_id} do
%{conn: conn} = oauth_access(["read"])
res_conn = get(conn, "/api/v1/statuses/#{reply_id}/context")
%{"ancestors" => ancestors, "descendants" => descendants} =
json_response_and_validate_schema(res_conn, 200)
ancestor_ids =
ancestors
|> Enum.map(& &1["id"])
descendant_ids =
descendants
|> Enum.map(& &1["id"])
assert post_id in ancestor_ids
assert remote_reply_id in descendant_ids
end
end
describe "getting status contexts restricted unauthenticated for remote" do
setup do: local_and_remote_context_activities()
setup do: clear_config([:restrict_unauthenticated, :activities, :local], false)
setup do: clear_config([:restrict_unauthenticated, :activities, :remote], true)
test "if user is unauthenticated", %{
conn: conn,
locals: [post_id, reply_id],
remote: remote_reply_id
} do
res_conn = get(conn, "/api/v1/statuses/#{post_id}/context")
%{"ancestors" => [], "descendants" => descendants} =
json_response_and_validate_schema(res_conn, 200)
descendant_ids =
descendants
|> Enum.map(& &1["id"])
assert reply_id in descendant_ids
assert remote_reply_id not in descendant_ids
end
test "if user is unauthenticated reply", %{
conn: conn,
locals: [post_id, reply_id],
remote: remote_reply_id
} do
res_conn = get(conn, "/api/v1/statuses/#{reply_id}/context")
%{"ancestors" => ancestors, "descendants" => descendants} =
json_response_and_validate_schema(res_conn, 200)
ancestor_ids =
ancestors
|> Enum.map(& &1["id"])
descendant_ids =
descendants
|> Enum.map(& &1["id"])
assert post_id in ancestor_ids
assert remote_reply_id not in descendant_ids
end
test "if user is authenticated", %{locals: [post_id, reply_id], remote: remote_reply_id} do
%{conn: conn} = oauth_access(["read"])
res_conn = get(conn, "/api/v1/statuses/#{post_id}/context")
%{"ancestors" => [], "descendants" => descendants} =
json_response_and_validate_schema(res_conn, 200)
reply_ids =
descendants
|> Enum.map(& &1["id"])
assert reply_id in reply_ids
assert remote_reply_id in reply_ids
end
test "if user is authenticated reply", %{locals: [post_id, reply_id], remote: remote_reply_id} do
%{conn: conn} = oauth_access(["read"])
res_conn = get(conn, "/api/v1/statuses/#{reply_id}/context")
%{"ancestors" => ancestors, "descendants" => descendants} =
json_response_and_validate_schema(res_conn, 200)
ancestor_ids =
ancestors
|> Enum.map(& &1["id"])
descendant_ids =
descendants
|> Enum.map(& &1["id"])
assert post_id in ancestor_ids
assert remote_reply_id in descendant_ids
end
end
describe "deleting a status" do
test "when you created it" do
%{user: author, conn: conn} = oauth_access(["write:statuses"])
activity = insert(:note_activity, user: author)
object = Object.normalize(activity, fetch: false)
content = object.data["content"]
source = object.data["source"]
result =
conn
|> assign(:user, author)
|> delete("/api/v1/statuses/#{activity.id}")
|> json_response_and_validate_schema(200)
assert match?(%{"content" => ^content, "text" => ^source}, result)
refute Activity.get_by_id(activity.id)
end
test "when it doesn't exist" do
%{user: author, conn: conn} = oauth_access(["write:statuses"])
activity = insert(:note_activity, user: author)
conn =
conn
|> assign(:user, author)
|> delete("/api/v1/statuses/#{String.downcase(activity.id)}")
assert %{"error" => "Record not found"} == json_response_and_validate_schema(conn, 404)
end
test "when you didn't create it" do
%{conn: conn} = oauth_access(["write:statuses"])
activity = insert(:note_activity)
conn = delete(conn, "/api/v1/statuses/#{activity.id}")
assert %{"error" => "Record not found"} == json_response_and_validate_schema(conn, 404)
assert Activity.get_by_id(activity.id) == activity
end
test "when you're privileged to", %{conn: conn} do
clear_config([:instance, :moderator_privileges], [:messages_delete])
activity = insert(:note_activity)
user = insert(:user, is_moderator: true)
res_conn =
conn
|> assign(:user, user)
|> assign(:token, insert(:oauth_token, user: user, scopes: ["write:statuses"]))
|> delete("/api/v1/statuses/#{activity.id}")
assert %{} = json_response_and_validate_schema(res_conn, 200)
assert ModerationLog |> Repo.one() |> ModerationLog.get_log_entry_message() ==
"@#{user.nickname} deleted status ##{activity.id}"
refute Activity.get_by_id(activity.id)
end
test "when you're privileged and the user is banned", %{conn: conn} do
clear_config([:instance, :moderator_privileges], [:messages_delete])
posting_user = insert(:user, is_active: false)
refute posting_user.is_active
activity = insert(:note_activity, user: posting_user)
user = insert(:user, is_moderator: true)
res_conn =
conn
|> assign(:user, user)
|> assign(:token, insert(:oauth_token, user: user, scopes: ["write:statuses"]))
|> delete("/api/v1/statuses/#{activity.id}")
assert %{} = json_response_and_validate_schema(res_conn, 200)
assert ModerationLog |> Repo.one() |> ModerationLog.get_log_entry_message() ==
"@#{user.nickname} deleted status ##{activity.id}"
refute Activity.get_by_id(activity.id)
end
end
describe "reblogging" do
setup do: oauth_access(["write:statuses"])
test "reblogs and returns the reblogged status", %{conn: conn} do
activity = insert(:note_activity)
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/reblog")
assert %{
"reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1},
"reblogged" => true
} = json_response_and_validate_schema(conn, 200)
assert to_string(activity.id) == id
end
test "returns 404 if the reblogged status doesn't exist", %{conn: conn} do
activity = insert(:note_activity)
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{String.downcase(activity.id)}/reblog")
assert %{"error" => "Record not found"} = json_response_and_validate_schema(conn, 404)
end
test "reblogs privately and returns the reblogged status", %{conn: conn} do
activity = insert(:note_activity)
conn =
conn
|> put_req_header("content-type", "application/json")
|> post(
"/api/v1/statuses/#{activity.id}/reblog",
%{"visibility" => "private"}
)
assert %{
"reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1},
"reblogged" => true,
"visibility" => "private"
} = json_response_and_validate_schema(conn, 200)
assert to_string(activity.id) == id
end
test "reblogged status for another user" do
activity = insert(:note_activity)
user1 = insert(:user)
user2 = insert(:user)
user3 = insert(:user)
{:ok, _} = CommonAPI.favorite(user2, activity.id)
{:ok, _bookmark} = Pleroma.Bookmark.create(user2.id, activity.id)
{:ok, reblog_activity1} = CommonAPI.repeat(activity.id, user1)
{:ok, _} = CommonAPI.repeat(activity.id, user2)
conn_res =
build_conn()
|> assign(:user, user3)
|> assign(:token, insert(:oauth_token, user: user3, scopes: ["read:statuses"]))
|> get("/api/v1/statuses/#{reblog_activity1.id}")
assert %{
"reblog" => %{"id" => _id, "reblogged" => false, "reblogs_count" => 2},
"reblogged" => false,
"favourited" => false,
"bookmarked" => false
} = json_response_and_validate_schema(conn_res, 200)
conn_res =
build_conn()
|> assign(:user, user2)
|> assign(:token, insert(:oauth_token, user: user2, scopes: ["read:statuses"]))
|> get("/api/v1/statuses/#{reblog_activity1.id}")
assert %{
"reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 2},
"reblogged" => true,
"favourited" => true,
"bookmarked" => true
} = json_response_and_validate_schema(conn_res, 200)
assert to_string(activity.id) == id
end
test "author can reblog own private status", %{conn: conn, user: user} do
{:ok, activity} = CommonAPI.post(user, %{status: "cofe", visibility: "private"})
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/reblog")
assert %{
"reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1},
"reblogged" => true,
"visibility" => "private"
} = json_response_and_validate_schema(conn, 200)
assert to_string(activity.id) == id
end
end
describe "unreblogging" do
setup do: oauth_access(["write:statuses"])
test "unreblogs and returns the unreblogged status", %{user: user, conn: conn} do
activity = insert(:note_activity)
{:ok, _} = CommonAPI.repeat(activity.id, user)
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/unreblog")
assert %{"id" => id, "reblogged" => false, "reblogs_count" => 0} =
json_response_and_validate_schema(conn, 200)
assert to_string(activity.id) == id
end
test "returns 404 error when activity does not exist", %{conn: conn} do
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/foo/unreblog")
assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"}
end
end
describe "favoriting" do
setup do: oauth_access(["write:favourites"])
test "favs a status and returns it", %{conn: conn} do
activity = insert(:note_activity)
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/favourite")
assert %{"id" => id, "favourites_count" => 1, "favourited" => true} =
json_response_and_validate_schema(conn, 200)
assert to_string(activity.id) == id
end
test "favoriting twice will just return 200", %{conn: conn} do
activity = insert(:note_activity)
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/favourite")
assert conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/favourite")
|> json_response_and_validate_schema(200)
end
test "returns 404 error for a wrong id", %{conn: conn} do
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/1/favourite")
assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"}
end
end
describe "unfavoriting" do
setup do: oauth_access(["write:favourites"])
test "unfavorites a status and returns it", %{user: user, conn: conn} do
activity = insert(:note_activity)
{:ok, _} = CommonAPI.favorite(user, activity.id)
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/unfavourite")
assert %{"id" => id, "favourites_count" => 0, "favourited" => false} =
json_response_and_validate_schema(conn, 200)
assert to_string(activity.id) == id
end
test "returns 404 error for a wrong id", %{conn: conn} do
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/1/unfavourite")
assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"}
end
end
describe "pinned statuses" do
setup do: oauth_access(["write:accounts"])
setup %{user: user} do
{:ok, activity} = CommonAPI.post(user, %{status: "HI!!!"})
%{activity: activity}
end
setup do: clear_config([:instance, :max_pinned_statuses], 1)
test "pin status", %{conn: conn, user: user, activity: activity} do
id = activity.id
assert %{"id" => ^id, "pinned" => true} =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/pin")
|> json_response_and_validate_schema(200)
assert [%{"id" => ^id, "pinned" => true}] =
conn
|> get("/api/v1/accounts/#{user.id}/statuses?pinned=true")
|> json_response_and_validate_schema(200)
end
test "non authenticated user", %{activity: activity} do
assert build_conn()
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/pin")
|> json_response(403) == %{"error" => "Invalid credentials."}
end
test "/pin: returns 400 error when activity is not public", %{conn: conn, user: user} do
{:ok, dm} = CommonAPI.post(user, %{status: "test", visibility: "direct"})
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{dm.id}/pin")
assert json_response_and_validate_schema(conn, 422) == %{
"error" => "Non-public status cannot be pinned"
}
end
test "pin by another user", %{activity: activity} do
%{conn: conn} = oauth_access(["write:accounts"])
assert conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/pin")
|> json_response(422) == %{"error" => "Someone else's status cannot be pinned"}
end
test "unpin status", %{conn: conn, user: user, activity: activity} do
{:ok, _} = CommonAPI.pin(activity.id, user)
user = refresh_record(user)
id_str = to_string(activity.id)
assert %{"id" => ^id_str, "pinned" => false} =
conn
|> assign(:user, user)
|> post("/api/v1/statuses/#{activity.id}/unpin")
|> json_response_and_validate_schema(200)
assert [] =
conn
|> get("/api/v1/accounts/#{user.id}/statuses?pinned=true")
|> json_response_and_validate_schema(200)
end
test "/unpin: returns 404 error when activity doesn't exist", %{conn: conn} do
assert conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/1/unpin")
|> json_response_and_validate_schema(404) == %{"error" => "Record not found"}
end
test "max pinned statuses", %{conn: conn, user: user, activity: activity_one} do
{:ok, activity_two} = CommonAPI.post(user, %{status: "HI!!!"})
id_str_one = to_string(activity_one.id)
assert %{"id" => ^id_str_one, "pinned" => true} =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{id_str_one}/pin")
|> json_response_and_validate_schema(200)
user = refresh_record(user)
assert %{"error" => "You have already pinned the maximum number of statuses"} =
conn
|> assign(:user, user)
|> post("/api/v1/statuses/#{activity_two.id}/pin")
|> json_response_and_validate_schema(400)
end
test "on pin removes deletion job, on unpin reschedule deletion" do
%{conn: conn} = oauth_access(["write:accounts", "write:statuses"])
expires_in = 2 * 60 * 60
expires_at = DateTime.add(DateTime.utc_now(), expires_in)
assert %{"id" => id} =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "oolong",
"expires_in" => expires_in
})
|> json_response_and_validate_schema(200)
assert_enqueued(
worker: Pleroma.Workers.PurgeExpiredActivity,
args: %{activity_id: id},
scheduled_at: expires_at
)
assert %{"id" => ^id, "pinned" => true} =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{id}/pin")
|> json_response_and_validate_schema(200)
refute_enqueued(
worker: Pleroma.Workers.PurgeExpiredActivity,
args: %{activity_id: id},
scheduled_at: expires_at
)
assert %{"id" => ^id, "pinned" => false} =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{id}/unpin")
|> json_response_and_validate_schema(200)
assert_enqueued(
worker: Pleroma.Workers.PurgeExpiredActivity,
args: %{activity_id: id},
scheduled_at: expires_at
)
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"
%{conn: conn} = oauth_access(["write:bookmarks", "read:bookmarks"])
author = insert(:user)
{:ok, activity1} = CommonAPI.post(author, %{status: "heweoo?"})
{:ok, activity2} = CommonAPI.post(author, %{status: "heweoo!"})
response1 =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity1.id}/bookmark")
assert json_response_and_validate_schema(response1, 200)["bookmarked"] == true
response2 =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity2.id}/bookmark")
assert json_response_and_validate_schema(response2, 200)["bookmarked"] == true
bookmarks = get(conn, bookmarks_uri)
assert [
json_response_and_validate_schema(response2, 200),
json_response_and_validate_schema(response1, 200)
] ==
json_response_and_validate_schema(bookmarks, 200)
response1 =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity1.id}/unbookmark")
assert json_response_and_validate_schema(response1, 200)["bookmarked"] == false
bookmarks = get(conn, bookmarks_uri)
assert [json_response_and_validate_schema(response2, 200)] ==
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"])
setup do
post_user = insert(:user)
{:ok, activity} = CommonAPI.post(post_user, %{status: "HIE"})
%{activity: activity}
end
test "mute conversation", %{conn: conn, activity: activity} do
id_str = to_string(activity.id)
assert %{"id" => ^id_str, "muted" => true} =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/mute")
|> json_response_and_validate_schema(200)
end
test "cannot mute already muted conversation", %{conn: conn, user: user, activity: activity} do
{:ok, _} = CommonAPI.add_mute(user, activity)
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/mute")
assert json_response_and_validate_schema(conn, 400) == %{
"error" => "conversation is already muted"
}
end
test "unmute conversation", %{conn: conn, user: user, activity: activity} do
{:ok, _} = CommonAPI.add_mute(user, activity)
id_str = to_string(activity.id)
assert %{"id" => ^id_str, "muted" => false} =
conn
# |> assign(:user, user)
|> post("/api/v1/statuses/#{activity.id}/unmute")
|> json_response_and_validate_schema(200)
end
end
test "Repeated posts that are replies incorrectly have in_reply_to_id null", %{conn: conn} do
user1 = insert(:user)
user2 = insert(:user)
user3 = insert(:user)
{:ok, replied_to} = CommonAPI.post(user1, %{status: "cofe"})
# Reply to status from another user
conn1 =
conn
|> assign(:user, user2)
|> assign(:token, insert(:oauth_token, user: user2, scopes: ["write:statuses"]))
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => replied_to.id})
assert %{"content" => "xD", "id" => id} = json_response_and_validate_schema(conn1, 200)
activity = Activity.get_by_id_with_object(id)
assert Object.normalize(activity, fetch: false).data["inReplyTo"] ==
Object.normalize(replied_to, fetch: false).data["id"]
assert Activity.get_in_reply_to_activity(activity).id == replied_to.id
# Reblog from the third user
conn2 =
conn
|> assign(:user, user3)
|> assign(:token, insert(:oauth_token, user: user3, scopes: ["write:statuses"]))
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses/#{activity.id}/reblog")
assert %{"reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1}} =
json_response_and_validate_schema(conn2, 200)
assert to_string(activity.id) == id
# Getting third user status
conn3 =
conn
|> assign(:user, user3)
|> assign(:token, insert(:oauth_token, user: user3, scopes: ["read:statuses"]))
|> get("/api/v1/timelines/home")
[reblogged_activity] = json_response_and_validate_schema(conn3, 200)
assert reblogged_activity["reblog"]["in_reply_to_id"] == replied_to.id
replied_to_user = User.get_by_ap_id(replied_to.data["actor"])
assert reblogged_activity["reblog"]["in_reply_to_account_id"] == replied_to_user.id
end
describe "GET /api/v1/statuses/:id/favourited_by" do
setup do: oauth_access(["read:accounts"])
setup %{user: user} do
{:ok, activity} = CommonAPI.post(user, %{status: "test"})
%{activity: activity}
end
test "returns users who have favorited the status", %{conn: conn, activity: activity} do
other_user = insert(:user)
{:ok, _} = CommonAPI.favorite(other_user, activity.id)
response =
conn
|> get("/api/v1/statuses/#{activity.id}/favourited_by")
|> json_response_and_validate_schema(:ok)
[%{"id" => id}] = response
assert id == other_user.id
end
test "returns empty array when status has not been favorited yet", %{
conn: conn,
activity: activity
} do
response =
conn
|> get("/api/v1/statuses/#{activity.id}/favourited_by")
|> json_response_and_validate_schema(:ok)
assert Enum.empty?(response)
end
test "does not return users who have favorited the status but are blocked", %{
conn: %{assigns: %{user: user}} = conn,
activity: activity
} do
other_user = insert(:user)
{:ok, _user_relationship} = User.block(user, other_user)
{:ok, _} = CommonAPI.favorite(other_user, activity.id)
response =
conn
|> get("/api/v1/statuses/#{activity.id}/favourited_by")
|> json_response_and_validate_schema(:ok)
assert Enum.empty?(response)
end
test "does not fail on an unauthenticated request", %{activity: activity} do
other_user = insert(:user)
{:ok, _} = CommonAPI.favorite(other_user, activity.id)
response =
build_conn()
|> get("/api/v1/statuses/#{activity.id}/favourited_by")
|> json_response_and_validate_schema(:ok)
[%{"id" => id}] = response
assert id == other_user.id
end
test "requires authentication for private posts", %{user: user} do
other_user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
status: "@#{other_user.nickname} wanna get some #cofe together?",
visibility: "direct"
})
{:ok, _} = CommonAPI.favorite(other_user, activity.id)
favourited_by_url = "/api/v1/statuses/#{activity.id}/favourited_by"
build_conn()
|> get(favourited_by_url)
|> json_response_and_validate_schema(404)
conn =
build_conn()
|> assign(:user, other_user)
|> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:accounts"]))
conn
|> assign(:token, nil)
|> get(favourited_by_url)
|> json_response_and_validate_schema(404)
response =
conn
|> get(favourited_by_url)
|> json_response_and_validate_schema(200)
[%{"id" => id}] = response
assert id == other_user.id
end
test "returns empty array when :show_reactions is disabled", %{conn: conn, activity: activity} do
clear_config([:instance, :show_reactions], false)
other_user = insert(:user)
{:ok, _} = CommonAPI.favorite(other_user, activity.id)
response =
conn
|> get("/api/v1/statuses/#{activity.id}/favourited_by")
|> json_response_and_validate_schema(:ok)
assert Enum.empty?(response)
end
end
describe "GET /api/v1/statuses/:id/reblogged_by" do
setup do: oauth_access(["read:accounts"])
setup %{user: user} do
{:ok, activity} = CommonAPI.post(user, %{status: "test"})
%{activity: activity}
end
test "returns users who have reblogged the status", %{conn: conn, activity: activity} do
other_user = insert(:user)
{:ok, _} = CommonAPI.repeat(activity.id, other_user)
response =
conn
|> get("/api/v1/statuses/#{activity.id}/reblogged_by")
|> json_response_and_validate_schema(:ok)
[%{"id" => id}] = response
assert id == other_user.id
end
test "returns empty array when status has not been reblogged yet", %{
conn: conn,
activity: activity
} do
response =
conn
|> get("/api/v1/statuses/#{activity.id}/reblogged_by")
|> json_response_and_validate_schema(:ok)
assert Enum.empty?(response)
end
test "does not return users who have reblogged the status but are blocked", %{
conn: %{assigns: %{user: user}} = conn,
activity: activity
} do
other_user = insert(:user)
{:ok, _user_relationship} = User.block(user, other_user)
{:ok, _} = CommonAPI.repeat(activity.id, other_user)
response =
conn
|> get("/api/v1/statuses/#{activity.id}/reblogged_by")
|> json_response_and_validate_schema(:ok)
assert Enum.empty?(response)
end
test "does not return users who have reblogged the status privately", %{
conn: conn
} do
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{status: "my secret post"})
{:ok, _} = CommonAPI.repeat(activity.id, other_user, %{visibility: "private"})
response =
conn
|> get("/api/v1/statuses/#{activity.id}/reblogged_by")
|> json_response_and_validate_schema(:ok)
assert Enum.empty?(response)
end
test "does not fail on an unauthenticated request", %{activity: activity} do
other_user = insert(:user)
{:ok, _} = CommonAPI.repeat(activity.id, other_user)
response =
build_conn()
|> get("/api/v1/statuses/#{activity.id}/reblogged_by")
|> json_response_and_validate_schema(:ok)
[%{"id" => id}] = response
assert id == other_user.id
end
test "requires authentication for private posts", %{user: user} do
other_user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
status: "@#{other_user.nickname} wanna get some #cofe together?",
visibility: "direct"
})
build_conn()
|> get("/api/v1/statuses/#{activity.id}/reblogged_by")
|> json_response_and_validate_schema(404)
response =
build_conn()
|> assign(:user, other_user)
|> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:accounts"]))
|> get("/api/v1/statuses/#{activity.id}/reblogged_by")
|> json_response_and_validate_schema(200)
assert [] == response
end
end
test "context" do
user = insert(:user)
{:ok, %{id: id1}} = CommonAPI.post(user, %{status: "1"})
{:ok, %{id: id2}} = CommonAPI.post(user, %{status: "2", in_reply_to_status_id: id1})
{:ok, %{id: id3}} = CommonAPI.post(user, %{status: "3", in_reply_to_status_id: id2})
{:ok, %{id: id4}} = CommonAPI.post(user, %{status: "4", in_reply_to_status_id: id3})
{:ok, %{id: id5}} = CommonAPI.post(user, %{status: "5", in_reply_to_status_id: id4})
response =
build_conn()
|> get("/api/v1/statuses/#{id3}/context")
|> json_response_and_validate_schema(:ok)
assert %{
"ancestors" => [%{"id" => ^id1}, %{"id" => ^id2}],
"descendants" => [%{"id" => ^id4}, %{"id" => ^id5}]
} = response
end
test "favorites paginate correctly" do
%{user: user, conn: conn} = oauth_access(["read:favourites"])
other_user = insert(:user)
{:ok, first_post} = CommonAPI.post(other_user, %{status: "bla"})
{:ok, second_post} = CommonAPI.post(other_user, %{status: "bla"})
{:ok, third_post} = CommonAPI.post(other_user, %{status: "bla"})
{:ok, _first_favorite} = CommonAPI.favorite(user, third_post.id)
{:ok, _second_favorite} = CommonAPI.favorite(user, first_post.id)
{:ok, third_favorite} = CommonAPI.favorite(user, second_post.id)
result =
conn
|> get("/api/v1/favourites?limit=1")
assert [%{"id" => post_id}] = json_response_and_validate_schema(result, 200)
assert post_id == second_post.id
# Using the header for pagination works correctly
[next, _] = get_resp_header(result, "link") |> hd() |> String.split(", ")
[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
result =
conn
|> get("/api/v1/favourites?max_id=#{max_id}")
assert [%{"id" => first_post_id}, %{"id" => third_post_id}] =
json_response_and_validate_schema(result, 200)
assert first_post_id == first_post.id
assert third_post_id == third_post.id
end
test "returns the favorites of a user" do
%{user: user, conn: conn} = oauth_access(["read:favourites"])
other_user = insert(:user)
{:ok, _} = CommonAPI.post(other_user, %{status: "bla"})
{:ok, activity} = CommonAPI.post(other_user, %{status: "trees are happy"})
{:ok, last_like} = CommonAPI.favorite(user, activity.id)
first_conn = get(conn, "/api/v1/favourites")
assert [status] = json_response_and_validate_schema(first_conn, 200)
assert status["id"] == to_string(activity.id)
assert [{"link", _link_header}] =
Enum.filter(first_conn.resp_headers, fn element -> match?({"link", _}, element) end)
# Honours query params
{:ok, second_activity} =
CommonAPI.post(other_user, %{
status: "Trees Are Never Sad Look At Them Every Once In Awhile They're Quite Beautiful."
})
{:ok, _} = CommonAPI.favorite(user, second_activity.id)
second_conn = get(conn, "/api/v1/favourites?since_id=#{last_like.id}")
assert [second_status] = json_response_and_validate_schema(second_conn, 200)
assert second_status["id"] == to_string(second_activity.id)
third_conn = get(conn, "/api/v1/favourites?limit=0")
assert [] = json_response_and_validate_schema(third_conn, 200)
end
test "expires_at is nil for another user" do
%{conn: conn, user: user} = oauth_access(["read:statuses"])
expires_at = DateTime.add(DateTime.utc_now(), 1_000_000)
{:ok, activity} = CommonAPI.post(user, %{status: "foobar", expires_in: 1_000_000})
assert %{"pleroma" => %{"expires_at" => a_expires_at}} =
conn
|> get("/api/v1/statuses/#{activity.id}")
|> json_response_and_validate_schema(:ok)
{:ok, a_expires_at, 0} = DateTime.from_iso8601(a_expires_at)
assert DateTime.diff(expires_at, a_expires_at) == 0
%{conn: conn} = oauth_access(["read:statuses"])
assert %{"pleroma" => %{"expires_at" => nil}} =
conn
|> get("/api/v1/statuses/#{activity.id}")
|> json_response_and_validate_schema(:ok)
end
describe "local-only statuses" do
test "posting a local only status" do
%{user: _user, conn: conn} = oauth_access(["write:statuses"])
conn_one =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "cofe",
"visibility" => "local"
})
local = Utils.as_local_public()
assert %{"content" => "cofe", "id" => id, "visibility" => "local"} =
json_response_and_validate_schema(conn_one, 200)
assert %Activity{id: ^id, data: %{"to" => [^local]}} = Activity.get_by_id(id)
end
test "other users can read local-only posts" do
user = insert(:user)
%{user: _reader, conn: conn} = oauth_access(["read:statuses"])
{:ok, activity} = CommonAPI.post(user, %{status: "#2hu #2HU", visibility: "local"})
received =
conn
|> get("/api/v1/statuses/#{activity.id}")
|> json_response_and_validate_schema(:ok)
assert received["id"] == activity.id
end
test "anonymous users cannot see local-only posts" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "#2hu #2HU", visibility: "local"})
_received =
build_conn()
|> get("/api/v1/statuses/#{activity.id}")
|> json_response_and_validate_schema(:not_found)
end
end
describe "muted reactions" do
test "index" do
%{conn: conn, user: user} = oauth_access(["read:statuses"])
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "test"})
{:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅")
User.mute(user, other_user)
result =
conn
|> get("/api/v1/statuses/?ids[]=#{activity.id}")
|> json_response_and_validate_schema(200)
assert [
%{
"pleroma" => %{
"emoji_reactions" => []
}
}
] = result
result =
conn
|> get("/api/v1/statuses/?ids[]=#{activity.id}&with_muted=true")
|> json_response_and_validate_schema(200)
assert [
%{
"pleroma" => %{
"emoji_reactions" => [%{"count" => 1, "me" => false, "name" => "🎅"}]
}
}
] = result
end
test "show" do
# %{conn: conn, user: user, token: token} = oauth_access(["read:statuses"])
%{conn: conn, user: user, token: _token} = oauth_access(["read:statuses"])
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "test"})
{:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅")
User.mute(user, other_user)
result =
conn
|> get("/api/v1/statuses/#{activity.id}")
|> json_response_and_validate_schema(200)
assert %{
"pleroma" => %{
"emoji_reactions" => []
}
} = result
result =
conn
|> get("/api/v1/statuses/#{activity.id}?with_muted=true")
|> json_response_and_validate_schema(200)
assert %{
"pleroma" => %{
"emoji_reactions" => [%{"count" => 1, "me" => false, "name" => "🎅"}]
}
} = result
end
end
describe "get status history" do
setup do
%{conn: build_conn()}
end
test "unedited post", %{conn: conn} do
activity = insert(:note_activity)
conn = get(conn, "/api/v1/statuses/#{activity.id}/history")
assert [_] = json_response_and_validate_schema(conn, 200)
end
test "edited post", %{conn: conn} do
note =
insert(
:note,
data: %{
"formerRepresentations" => %{
"type" => "OrderedCollection",
"orderedItems" => [
%{
"type" => "Note",
"content" => "mew mew 2",
"summary" => "title 2"
},
%{
"type" => "Note",
"content" => "mew mew 1",
"summary" => "title 1"
}
],
"totalItems" => 2
}
}
)
activity = insert(:note_activity, note: note)
conn = get(conn, "/api/v1/statuses/#{activity.id}/history")
assert [%{"spoiler_text" => "title 1"}, %{"spoiler_text" => "title 2"}, _] =
json_response_and_validate_schema(conn, 200)
end
end
describe "get status source" do
setup do
%{conn: build_conn()}
end
test "it returns the source", %{conn: conn} do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "mew mew #abc", spoiler_text: "#def"})
conn = get(conn, "/api/v1/statuses/#{activity.id}/source")
id = activity.id
assert %{"id" => ^id, "text" => "mew mew #abc", "spoiler_text" => "#def"} =
json_response_and_validate_schema(conn, 200)
end
end
describe "update status" do
setup do
oauth_access(["write:statuses"])
end
test "it updates the status" do
%{conn: conn, user: user} = oauth_access(["write:statuses", "read:statuses"])
{:ok, activity} = CommonAPI.post(user, %{status: "mew mew #abc", spoiler_text: "#def"})
conn
|> get("/api/v1/statuses/#{activity.id}")
|> json_response_and_validate_schema(200)
response =
conn
|> put_req_header("content-type", "application/json")
|> put("/api/v1/statuses/#{activity.id}", %{
"status" => "edited",
"spoiler_text" => "lol"
})
|> json_response_and_validate_schema(200)
assert response["content"] == "edited"
assert response["spoiler_text"] == "lol"
response =
conn
|> get("/api/v1/statuses/#{activity.id}")
|> json_response_and_validate_schema(200)
assert response["content"] == "edited"
assert response["spoiler_text"] == "lol"
end
test "it updates the attachments", %{conn: conn, user: user} do
attachment = insert(:attachment, user: user)
attachment_id = to_string(attachment.id)
{:ok, activity} = CommonAPI.post(user, %{status: "mew mew #abc", spoiler_text: "#def"})
response =
conn
|> put_req_header("content-type", "application/json")
|> put("/api/v1/statuses/#{activity.id}", %{
"status" => "mew mew #abc",
"spoiler_text" => "#def",
"media_ids" => [attachment_id]
})
|> json_response_and_validate_schema(200)
assert [%{"id" => ^attachment_id}] = response["media_attachments"]
end
test "it does not update visibility", %{conn: conn, user: user} do
{:ok, activity} =
CommonAPI.post(user, %{
status: "mew mew #abc",
spoiler_text: "#def",
visibility: "private"
})
response =
conn
|> put_req_header("content-type", "application/json")
|> put("/api/v1/statuses/#{activity.id}", %{
"status" => "edited",
"spoiler_text" => "lol"
})
|> json_response_and_validate_schema(200)
assert response["visibility"] == "private"
end
test "it refuses to update when original post is not by the user", %{conn: conn} do
another_user = insert(:user)
{:ok, activity} =
CommonAPI.post(another_user, %{status: "mew mew #abc", spoiler_text: "#def"})
conn
|> put_req_header("content-type", "application/json")
|> put("/api/v1/statuses/#{activity.id}", %{
"status" => "edited",
"spoiler_text" => "lol"
})
|> json_response_and_validate_schema(:forbidden)
end
test "it returns 404 if the user cannot see the post", %{conn: conn} do
another_user = insert(:user)
{:ok, activity} =
CommonAPI.post(another_user, %{
status: "mew mew #abc",
spoiler_text: "#def",
visibility: "private"
})
conn
|> put_req_header("content-type", "application/json")
|> put("/api/v1/statuses/#{activity.id}", %{
"status" => "edited",
"spoiler_text" => "lol"
})
|> json_response_and_validate_schema(:not_found)
end
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
index f007bb317..663610442 100644
--- a/test/pleroma/web/mastodon_api/views/status_view_test.exs
+++ b/test/pleroma/web/mastodon_api/views/status_view_test.exs
@@ -1,932 +1,932 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
use Pleroma.DataCase
alias Pleroma.Activity
alias Pleroma.Bookmark
alias Pleroma.Conversation.Participation
alias Pleroma.HTML
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.UnstubbedConfigMock, as: ConfigMock
alias Pleroma.User
alias Pleroma.UserRelationship
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.StatusView
+ alias Pleroma.Web.RichMedia.Card
require Bitwise
import Mox
import OpenApiSpex.TestAssertions
import Pleroma.Factory
import Tesla.Mock
setup do
mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
test "has an emoji reaction list" do
user = insert(:user)
other_user = insert(:user)
third_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "dae cofe??"})
{:ok, _} = CommonAPI.react_with_emoji(activity.id, user, "☕")
{:ok, _} = CommonAPI.react_with_emoji(activity.id, user, ":dinosaur:")
{:ok, _} = CommonAPI.react_with_emoji(activity.id, third_user, "🍵")
{:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "☕")
{:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, ":dinosaur:")
activity = Repo.get(Activity, activity.id)
status = StatusView.render("show.json", activity: activity)
assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec())
assert status[:pleroma][:emoji_reactions] == [
%{name: "☕", count: 2, me: false, url: nil, account_ids: [other_user.id, user.id]},
%{
count: 2,
me: false,
name: "dinosaur",
url: "http://localhost:4001/emoji/dino walking.gif",
account_ids: [other_user.id, user.id]
},
%{name: "🍵", count: 1, me: false, url: nil, account_ids: [third_user.id]}
]
status = StatusView.render("show.json", activity: activity, for: user)
assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec())
assert status[:pleroma][:emoji_reactions] == [
%{name: "☕", count: 2, me: true, url: nil, account_ids: [other_user.id, user.id]},
%{
count: 2,
me: true,
name: "dinosaur",
url: "http://localhost:4001/emoji/dino walking.gif",
account_ids: [other_user.id, user.id]
},
%{name: "🍵", count: 1, me: false, url: nil, account_ids: [third_user.id]}
]
end
test "works with legacy-formatted reactions" do
user = insert(:user)
other_user = insert(:user)
note =
insert(:note,
user: user,
data: %{
"reactions" => [["😿", [other_user.ap_id]]]
}
)
activity = insert(:note_activity, user: user, note: note)
status = StatusView.render("show.json", activity: activity, for: user)
assert status[:pleroma][:emoji_reactions] == [
%{name: "😿", count: 1, me: false, url: nil, account_ids: [other_user.id]}
]
end
test "works correctly with badly formatted emojis" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "yo"})
activity
|> Object.normalize(fetch: false)
|> Object.update_data(%{"reactions" => %{"☕" => [user.ap_id], "x" => 1}})
activity = Activity.get_by_id(activity.id)
status = StatusView.render("show.json", activity: activity, for: user)
assert status[:pleroma][:emoji_reactions] == [
%{name: "☕", count: 1, me: true, url: nil, account_ids: [user.id]}
]
end
test "doesn't show reactions from muted and blocked users" do
user = insert(:user)
other_user = insert(:user)
third_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "dae cofe??"})
{:ok, _} = User.mute(user, other_user)
{:ok, _} = User.block(other_user, third_user)
{:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "☕")
activity = Repo.get(Activity, activity.id)
status = StatusView.render("show.json", activity: activity)
assert status[:pleroma][:emoji_reactions] == [
%{name: "☕", count: 1, me: false, url: nil, account_ids: [other_user.id]}
]
status = StatusView.render("show.json", activity: activity, for: user)
assert status[:pleroma][:emoji_reactions] == []
{:ok, _} = CommonAPI.react_with_emoji(activity.id, third_user, "☕")
status = StatusView.render("show.json", activity: activity)
assert status[:pleroma][:emoji_reactions] == [
%{
name: "☕",
count: 2,
me: false,
url: nil,
account_ids: [third_user.id, other_user.id]
}
]
status = StatusView.render("show.json", activity: activity, for: user)
assert status[:pleroma][:emoji_reactions] == [
%{name: "☕", count: 1, me: false, url: nil, account_ids: [third_user.id]}
]
status = StatusView.render("show.json", activity: activity, for: other_user)
assert status[:pleroma][:emoji_reactions] == [
%{name: "☕", count: 1, me: true, url: nil, account_ids: [other_user.id]}
]
end
test "loads and returns the direct conversation id when given the `with_direct_conversation_id` option" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "Hey @shp!", visibility: "direct"})
[participation] = Participation.for_user(user)
status =
StatusView.render("show.json",
activity: activity,
with_direct_conversation_id: true,
for: user
)
assert status[:pleroma][:direct_conversation_id] == participation.id
status = StatusView.render("show.json", activity: activity, for: user)
assert status[:pleroma][:direct_conversation_id] == nil
assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec())
end
test "returns the direct conversation id when given the `direct_conversation_id` option" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "Hey @shp!", visibility: "direct"})
[participation] = Participation.for_user(user)
status =
StatusView.render("show.json",
activity: activity,
direct_conversation_id: participation.id,
for: user
)
assert status[:pleroma][:direct_conversation_id] == participation.id
assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec())
end
@tag capture_log: true
test "returns a temporary ap_id based user for activities missing db users" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "Hey @shp!", visibility: "direct"})
Repo.delete(user)
User.invalidate_cache(user)
finger_url =
"https://localhost/.well-known/webfinger?resource=acct:#{user.nickname}@localhost"
Tesla.Mock.mock_global(fn
%{method: :get, url: "http://localhost/.well-known/host-meta"} ->
%Tesla.Env{status: 404, body: ""}
%{method: :get, url: "https://localhost/.well-known/host-meta"} ->
%Tesla.Env{status: 404, body: ""}
%{
method: :get,
url: ^finger_url
} ->
%Tesla.Env{status: 404, body: ""}
end)
%{account: ms_user} = StatusView.render("show.json", activity: activity)
assert ms_user.acct == "erroruser@example.com"
end
test "tries to get a user by nickname if fetching by ap_id doesn't work" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "Hey @shp!", visibility: "direct"})
{:ok, user} =
user
|> Ecto.Changeset.change(%{ap_id: "#{user.ap_id}/extension/#{user.nickname}"})
|> Repo.update()
User.invalidate_cache(user)
result = StatusView.render("show.json", activity: activity)
assert result[:account][:id] == to_string(user.id)
assert_schema(result, "Status", Pleroma.Web.ApiSpec.spec())
end
test "a note with null content" do
note = insert(:note_activity)
note_object = Object.normalize(note, fetch: false)
data =
note_object.data
|> Map.put("content", nil)
Object.change(note_object, %{data: data})
|> Object.update_and_set_cache()
User.get_cached_by_ap_id(note.data["actor"])
status = StatusView.render("show.json", %{activity: note})
assert status.content == ""
assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec())
end
test "a note activity" do
note = insert(:note_activity)
object_data = Object.normalize(note, fetch: false).data
user = User.get_cached_by_ap_id(note.data["actor"])
convo_id = :erlang.crc32(object_data["context"]) |> Bitwise.band(Bitwise.bnot(0x8000_0000))
status = StatusView.render("show.json", %{activity: note})
created_at =
(object_data["published"] || "")
|> String.replace(~r/\.\d+Z/, ".000Z")
expected = %{
id: to_string(note.id),
uri: object_data["id"],
url: Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, note),
account: AccountView.render("show.json", %{user: user, skip_visibility_check: true}),
in_reply_to_id: nil,
in_reply_to_account_id: nil,
card: nil,
reblog: nil,
content: HTML.filter_tags(object_data["content"]),
text: nil,
created_at: created_at,
edited_at: nil,
reblogs_count: 0,
replies_count: 0,
favourites_count: 0,
reblogged: false,
bookmarked: false,
favourited: false,
muted: false,
pinned: false,
sensitive: false,
poll: nil,
spoiler_text: HTML.filter_tags(object_data["summary"]),
visibility: "public",
media_attachments: [],
mentions: [],
tags: [
%{
name: "#{hd(object_data["tag"])}",
url: "http://localhost:4001/tag/#{hd(object_data["tag"])}"
}
],
application: nil,
language: nil,
emojis: [
%{
shortcode: "2hu",
url: "corndog.png",
static_url: "corndog.png",
visible_in_picker: false
}
],
pleroma: %{
local: true,
conversation_id: convo_id,
context: object_data["context"],
in_reply_to_account_acct: nil,
quote: nil,
quote_id: nil,
quote_url: nil,
quote_visible: false,
content: %{"text/plain" => HTML.strip_tags(object_data["content"])},
spoiler_text: %{"text/plain" => HTML.strip_tags(object_data["summary"])},
expires_at: nil,
direct_conversation_id: nil,
thread_muted: false,
emoji_reactions: [],
parent_visible: false,
pinned_at: nil,
quotes_count: 0,
bookmark_folder: nil
}
}
assert status == expected
assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec())
end
test "tells if the message is muted for some reason" do
user = insert(:user)
other_user = insert(:user)
{:ok, _user_relationships} = User.mute(user, other_user)
{:ok, activity} = CommonAPI.post(other_user, %{status: "test"})
relationships_opt = UserRelationship.view_relationships_option(user, [other_user])
opts = %{activity: activity}
status = StatusView.render("show.json", opts)
assert status.muted == false
assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec())
status = StatusView.render("show.json", Map.put(opts, :relationships, relationships_opt))
assert status.muted == false
for_opts = %{activity: activity, for: user}
status = StatusView.render("show.json", for_opts)
assert status.muted == true
status = StatusView.render("show.json", Map.put(for_opts, :relationships, relationships_opt))
assert status.muted == true
assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec())
end
test "tells if the message is thread muted" do
user = insert(:user)
other_user = insert(:user)
{:ok, _user_relationships} = User.mute(user, other_user)
{:ok, activity} = CommonAPI.post(other_user, %{status: "test"})
status = StatusView.render("show.json", %{activity: activity, for: user})
assert status.pleroma.thread_muted == false
{:ok, activity} = CommonAPI.add_mute(user, activity)
status = StatusView.render("show.json", %{activity: activity, for: user})
assert status.pleroma.thread_muted == true
end
test "tells if the status is bookmarked" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "Cute girls doing cute things"})
status = StatusView.render("show.json", %{activity: activity})
assert status.bookmarked == false
status = StatusView.render("show.json", %{activity: activity, for: user})
assert status.bookmarked == false
{:ok, _bookmark} = Bookmark.create(user.id, activity.id)
activity = Activity.get_by_id_with_object(activity.id)
status = StatusView.render("show.json", %{activity: activity, for: user})
assert status.bookmarked == true
end
test "a reply" do
note = insert(:note_activity)
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "he", in_reply_to_status_id: note.id})
status = StatusView.render("show.json", %{activity: activity})
assert status.in_reply_to_id == to_string(note.id)
[status] = StatusView.render("index.json", %{activities: [activity], as: :activity})
assert status.in_reply_to_id == to_string(note.id)
end
test "a quote post" do
post = insert(:note_activity)
user = insert(:user)
{:ok, quote_post} = CommonAPI.post(user, %{status: "he", quote_id: post.id})
{:ok, quoted_quote_post} = CommonAPI.post(user, %{status: "yo", quote_id: quote_post.id})
status = StatusView.render("show.json", %{activity: quoted_quote_post})
assert status.pleroma.quote.id == to_string(quote_post.id)
assert status.pleroma.quote_id == to_string(quote_post.id)
assert status.pleroma.quote_url == Object.normalize(quote_post).data["id"]
assert status.pleroma.quote_visible
# Quotes don't go more than one level deep
refute status.pleroma.quote.pleroma.quote
assert status.pleroma.quote.pleroma.quote_id == to_string(post.id)
assert status.pleroma.quote.pleroma.quote_url == Object.normalize(post).data["id"]
assert status.pleroma.quote.pleroma.quote_visible
# In an index
[status] = StatusView.render("index.json", %{activities: [quoted_quote_post], as: :activity})
assert status.pleroma.quote.id == to_string(quote_post.id)
end
test "quoted private post" do
user = insert(:user)
# Insert a private post
private = insert(:followers_only_note_activity, user: user)
private_object = Object.normalize(private)
# Create a public post quoting the private post
quote_private =
insert(:note_activity, note: insert(:note, data: %{"quoteUrl" => private_object.data["id"]}))
status = StatusView.render("show.json", %{activity: quote_private})
# The quote isn't rendered
refute status.pleroma.quote
assert status.pleroma.quote_url == private_object.data["id"]
refute status.pleroma.quote_visible
# After following the user, the quote is rendered
follower = insert(:user)
CommonAPI.follow(follower, user)
status = StatusView.render("show.json", %{activity: quote_private, for: follower})
assert status.pleroma.quote.id == to_string(private.id)
assert status.pleroma.quote_visible
end
test "quoted direct message" do
# Insert a direct message
direct = insert(:direct_note_activity)
direct_object = Object.normalize(direct)
# Create a public post quoting the direct message
quote_direct =
insert(:note_activity, note: insert(:note, data: %{"quoteUrl" => direct_object.data["id"]}))
status = StatusView.render("show.json", %{activity: quote_direct})
# The quote isn't rendered
refute status.pleroma.quote
assert status.pleroma.quote_url == direct_object.data["id"]
refute status.pleroma.quote_visible
end
test "repost of quote post" do
post = insert(:note_activity)
user = insert(:user)
{:ok, quote_post} = CommonAPI.post(user, %{status: "he", quote_id: post.id})
{:ok, repost} = CommonAPI.repeat(quote_post.id, user)
[status] = StatusView.render("index.json", %{activities: [repost], as: :activity})
assert status.reblog.pleroma.quote.id == to_string(post.id)
end
test "contains mentions" do
user = insert(:user)
mentioned = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "hi @#{mentioned.nickname}"})
status = StatusView.render("show.json", %{activity: activity})
assert status.mentions ==
Enum.map([mentioned], fn u -> AccountView.render("mention.json", %{user: u}) end)
assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec())
end
test "create mentions from the 'to' field" do
%User{ap_id: recipient_ap_id} = insert(:user)
cc = insert_pair(:user) |> Enum.map(& &1.ap_id)
object =
insert(:note, %{
data: %{
"to" => [recipient_ap_id],
"cc" => cc
}
})
activity =
insert(:note_activity, %{
note: object,
recipients: [recipient_ap_id | cc]
})
assert length(activity.recipients) == 3
%{mentions: [mention] = mentions} = StatusView.render("show.json", %{activity: activity})
assert length(mentions) == 1
assert mention.url == recipient_ap_id
end
test "create mentions from the 'tag' field" do
recipient = insert(:user)
cc = insert_pair(:user) |> Enum.map(& &1.ap_id)
object =
insert(:note, %{
data: %{
"cc" => cc,
"tag" => [
%{
"href" => recipient.ap_id,
"name" => recipient.nickname,
"type" => "Mention"
},
%{
"href" => "https://example.com/search?tag=test",
"name" => "#test",
"type" => "Hashtag"
}
]
}
})
activity =
insert(:note_activity, %{
note: object,
recipients: [recipient.ap_id | cc]
})
assert length(activity.recipients) == 3
%{mentions: [mention] = mentions} = StatusView.render("show.json", %{activity: activity})
assert length(mentions) == 1
assert mention.url == recipient.ap_id
end
test "attachments" do
object = %{
"type" => "Image",
"url" => [
%{
"mediaType" => "image/png",
"href" => "someurl",
"width" => 200,
"height" => 100
}
],
"blurhash" => "UJJ8X[xYW,%Jtq%NNFbXB5j]IVM|9GV=WHRn",
"uuid" => 6
}
expected = %{
id: "1638338801",
type: "image",
url: "someurl",
remote_url: "someurl",
preview_url: "someurl",
text_url: "someurl",
description: nil,
pleroma: %{mime_type: "image/png"},
meta: %{original: %{width: 200, height: 100, aspect: 2}},
blurhash: "UJJ8X[xYW,%Jtq%NNFbXB5j]IVM|9GV=WHRn"
}
api_spec = Pleroma.Web.ApiSpec.spec()
assert expected == StatusView.render("attachment.json", %{attachment: object})
assert_schema(expected, "Attachment", api_spec)
# If theres a "id", use that instead of the generated one
object = Map.put(object, "id", 2)
result = StatusView.render("attachment.json", %{attachment: object})
assert %{id: "2"} = result
assert_schema(result, "Attachment", api_spec)
end
test "put the url advertised in the Activity in to the url attribute" do
id = "https://wedistribute.org/wp-json/pterotype/v1/object/85810"
[activity] = Activity.search(nil, id)
status = StatusView.render("show.json", %{activity: activity})
assert status.uri == id
assert status.url == "https://wedistribute.org/2019/07/mastodon-drops-ostatus/"
end
test "a reblog" do
user = insert(:user)
activity = insert(:note_activity)
{:ok, reblog} = CommonAPI.repeat(activity.id, user)
represented = StatusView.render("show.json", %{for: user, activity: reblog})
assert represented[:id] == to_string(reblog.id)
assert represented[:reblog][:id] == to_string(activity.id)
assert represented[:emojis] == []
assert_schema(represented, "Status", Pleroma.Web.ApiSpec.spec())
end
test "a peertube video" do
user = insert(:user)
{:ok, object} =
Pleroma.Object.Fetcher.fetch_object_from_id(
"https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3"
)
%Activity{} = activity = Activity.get_create_by_object_ap_id(object.data["id"])
represented = StatusView.render("show.json", %{for: user, activity: activity})
assert represented[:id] == to_string(activity.id)
assert length(represented[:media_attachments]) == 1
assert_schema(represented, "Status", Pleroma.Web.ApiSpec.spec())
end
test "funkwhale audio" do
user = insert(:user)
{:ok, object} =
Pleroma.Object.Fetcher.fetch_object_from_id(
"https://channels.tests.funkwhale.audio/federation/music/uploads/42342395-0208-4fee-a38d-259a6dae0871"
)
%Activity{} = activity = Activity.get_create_by_object_ap_id(object.data["id"])
represented = StatusView.render("show.json", %{for: user, activity: activity})
assert represented[:id] == to_string(activity.id)
assert length(represented[:media_attachments]) == 1
end
test "a Mobilizon event" do
user = insert(:user)
{:ok, object} =
Pleroma.Object.Fetcher.fetch_object_from_id(
"https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39"
)
%Activity{} = activity = Activity.get_create_by_object_ap_id(object.data["id"])
represented = StatusView.render("show.json", %{for: user, activity: activity})
assert represented[:id] == to_string(activity.id)
assert represented[:url] ==
"https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39"
assert represented[:content] ==
"<p><a href=\"https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39\">Mobilizon Launching Party</a></p><p>Mobilizon is now federated! 🎉</p><p></p><p>You can view this event from other instances if they are subscribed to mobilizon.org, and soon directly from Mastodon and Pleroma. It is possible that you may see some comments from other instances, including Mastodon ones, just below.</p><p></p><p>With a Mobilizon account on an instance, you may <strong>participate</strong> at events from other instances and <strong>add comments</strong> on events.</p><p></p><p>Of course, it&#39;s still <u>a work in progress</u>: if reports made from an instance on events and comments can be federated, you can&#39;t block people right now, and moderators actions are rather limited, but this <strong>will definitely get fixed over time</strong> until first stable version next year.</p><p></p><p>Anyway, if you want to come up with some feedback, head over to our forum or - if you feel you have technical skills and are familiar with it - on our Gitlab repository.</p><p></p><p>Also, to people that want to set Mobilizon themselves even though we really don&#39;t advise to do that for now, we have a little documentation but it&#39;s quite the early days and you&#39;ll probably need some help. No worries, you can chat with us on our Forum or though our Matrix channel.</p><p></p><p>Check our website for more informations and follow us on Twitter or Mastodon.</p>"
end
describe "build_tags/1" do
test "it returns a a dictionary tags" do
object_tags = [
"fediverse",
"mastodon",
"nextcloud",
%{
"href" => "https://kawen.space/users/lain",
"name" => "@lain@kawen.space",
"type" => "Mention"
}
]
assert StatusView.build_tags(object_tags) == [
%{name: "fediverse", url: "http://localhost:4001/tag/fediverse"},
%{name: "mastodon", url: "http://localhost:4001/tag/mastodon"},
%{name: "nextcloud", url: "http://localhost:4001/tag/nextcloud"}
]
end
end
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})
+ %{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)
+
+ %{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"
- card = %{
- url: page_url,
- site_name: "Example site name",
- title: "Example website"
+ 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)
+
+ %{provider_name: "example.com"} = 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)
+
+ %{provider_name: "example.com"} = StatusView.render("card.json", card)
end
test "a rich media card has all media proxied" do
clear_config([:media_proxy, :enabled], true)
clear_config([:media_preview_proxy, :enabled])
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\//)
assert String.match?(og["audio"], ~r/\/proxy\//)
assert String.match?(og["video"], ~r/\/proxy\//)
end
end
test "does not embed a relationship in the account" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
status: "drink more water"
})
result = StatusView.render("show.json", %{activity: activity, for: other_user})
assert result[:account][:pleroma][:relationship] == %{}
assert_schema(result, "Status", Pleroma.Web.ApiSpec.spec())
end
test "does not embed a relationship in the account in reposts" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
status: "˙˙ɐʎns"
})
{:ok, activity} = CommonAPI.repeat(activity.id, other_user)
result = StatusView.render("show.json", %{activity: activity, for: user})
assert result[:account][:pleroma][:relationship] == %{}
assert result[:reblog][:account][:pleroma][:relationship] == %{}
assert_schema(result, "Status", Pleroma.Web.ApiSpec.spec())
end
test "visibility/list" do
user = insert(:user)
{:ok, list} = Pleroma.List.create("foo", user)
{:ok, activity} = CommonAPI.post(user, %{status: "foobar", visibility: "list:#{list.id}"})
status = StatusView.render("show.json", activity: activity)
assert status.visibility == "list"
end
test "has a field for parent visibility" do
user = insert(:user)
poster = insert(:user)
{:ok, invisible} = CommonAPI.post(poster, %{status: "hey", visibility: "private"})
{:ok, visible} =
CommonAPI.post(poster, %{status: "hey", visibility: "private", in_reply_to_id: invisible.id})
status = StatusView.render("show.json", activity: visible, for: user)
refute status.pleroma.parent_visible
status = StatusView.render("show.json", activity: visible, for: poster)
assert status.pleroma.parent_visible
end
test "it shows edited_at" do
poster = insert(:user)
{:ok, post} = CommonAPI.post(poster, %{status: "hey"})
status = StatusView.render("show.json", activity: post)
refute status.edited_at
{:ok, _} = CommonAPI.update(poster, post, %{status: "mew mew"})
edited = Pleroma.Activity.normalize(post)
status = StatusView.render("show.json", activity: edited)
assert status.edited_at
end
test "with a source object" do
note =
insert(:note,
data: %{"source" => %{"content" => "object source", "mediaType" => "text/markdown"}}
)
activity = insert(:note_activity, note: note)
status = StatusView.render("show.json", activity: activity, with_source: true)
assert status.text == "object source"
end
describe "source.json" do
test "with a source object, renders both source and content type" do
note =
insert(:note,
data: %{"source" => %{"content" => "object source", "mediaType" => "text/markdown"}}
)
activity = insert(:note_activity, note: note)
status = StatusView.render("source.json", activity: activity)
assert status.text == "object source"
assert status.content_type == "text/markdown"
end
test "with a source string, renders source and put text/plain as the content type" do
note = insert(:note, data: %{"source" => "string source"})
activity = insert(:note_activity, note: note)
status = StatusView.render("source.json", activity: activity)
assert status.text == "string source"
assert status.content_type == "text/plain"
end
end
end
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
index c8b3cb391..f17add774 100644
--- 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
@@ -1,95 +1,87 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.PleromaAPI.ChatMessageReferenceViewTest do
alias Pleroma.NullCache
use Pleroma.DataCase, async: true
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
alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView
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)
ConfigMock
|> stub_with(Pleroma.Test.StaticConfig)
file = %Plug.Upload{
content_type: "image/jpeg",
path: Path.absname("test/fixtures/image.jpg"),
filename: "an_image.jpg"
}
{:ok, upload} = ActivityPub.upload(file, actor: recipient.ap_id)
{:ok, activity} =
CommonAPI.post_chat_message(user, recipient, "kippis :firefox:", idempotency_key: "123")
chat = Chat.get(user.id, recipient.ap_id)
object = Object.normalize(activity, fetch: false)
cm_ref = MessageReference.for_chat_and_object(chat, object)
id = cm_ref.id
Pleroma.CachexMock
|> stub(:get, fn
: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)
assert chat_message[:id] == cm_ref.id
assert chat_message[:content] == "kippis :firefox:"
assert chat_message[:account_id] == user.id
assert chat_message[:chat_id]
assert chat_message[:created_at]
assert chat_message[:unread] == false
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",
media_id: upload.id
)
object = Object.normalize(activity, fetch: false)
cm_ref = MessageReference.for_chat_and_object(chat, object)
chat_message_two = MessageReferenceView.render("show.json", chat_message_reference: cm_ref)
assert chat_message_two[:id] == cm_ref.id
assert chat_message_two[:content] == object.data["content"]
assert chat_message_two[:account_id] == recipient.id
assert chat_message_two[:chat_id] == chat_message[:chat_id]
assert chat_message_two[:attachment]
assert chat_message_two[:unread] == true
assert chat_message_two[:card]
end
end
diff --git a/test/pleroma/web/rich_media/card_test.exs b/test/pleroma/web/rich_media/card_test.exs
new file mode 100644
index 000000000..516ac9951
--- /dev/null
+++ 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
deleted file mode 100644
index 13d2341ad..000000000
--- a/test/pleroma/web/rich_media/helpers_test.exs
+++ /dev/null
@@ -1,137 +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: false
-
- 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_global(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
-
- test "recrawls URLs on updates" do
- original_url = "https://google.com/"
- updated_url = "https://yahoo.com/"
-
- Pleroma.StaticStubbedConfigMock
- |> stub(:get, fn
- [:rich_media, :enabled] -> true
- path -> Pleroma.Test.StaticConfig.get(path)
- end)
-
- user = insert(:user)
- {:ok, activity} = CommonAPI.post(user, %{status: "I like this site #{original_url}"})
-
- assert match?(
- %{page_url: ^original_url, rich_media: _},
- Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
- )
-
- {:ok, _} = CommonAPI.update(user, activity, %{status: "I like this site #{updated_url}"})
-
- activity = Pleroma.Activity.get_by_id(activity.id)
-
- assert match?(
- %{page_url: ^updated_url, rich_media: _},
- Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
- )
- end
-
- 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
index b90f7d9e2..cd8be8675 100644
--- 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
@@ -1,81 +1,98 @@
# 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.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"
{:ok, timestamp} =
Timex.now()
|> DateTime.truncate(:second)
|> Timex.format("{ISO:Basic:Z}")
# in seconds
valid_till = 30
metadata = construct_metadata(timestamp, valid_till, url)
expire_time =
Timex.parse!(timestamp, "{ISO:Basic:Z}") |> Timex.to_unix() |> Kernel.+(valid_till)
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
url = "https://pleroma.social/amz"
{:ok, timestamp} =
Timex.now()
|> DateTime.truncate(:second)
|> Timex.format("{ISO:Basic:Z}")
# in seconds
valid_till = 30
metadata = construct_metadata(timestamp, valid_till, url)
body = """
<meta name="twitter:card" content="Pleroma" />
<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
"https://pleroma.s3.ap-southeast-1.amazonaws.com/sachin%20%281%29%20_a%20-%25%2Aasdasd%20BNN%20bnnn%20.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIBLWWK6RGDQXDLJQ%2F20190716%2Fap-southeast-1%2Fs3%2Faws4_request&X-Amz-Date=#{timestamp}&X-Amz-Expires=#{valid_till}&X-Amz-Signature=04ffd6b98634f4b1bbabc62e0fac4879093cd54a6eed24fe8eb38e8369526bbf&X-Amz-SignedHeaders=host"
end
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
new file mode 100644
index 000000000..770968d47
--- /dev/null
+++ 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
index a05b89a2b..3fcb5c808 100644
--- a/test/pleroma/web/rich_media/parser_test.exs
+++ b/test/pleroma/web/rich_media/parser_test.exs
@@ -1,107 +1,130 @@
# 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.ParserTest do
- use Pleroma.DataCase, async: false
+ use Pleroma.DataCase
alias Pleroma.Web.RichMedia.Parser
import Tesla.Mock
setup do
mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
end
test "returns error when no metadata present" do
assert {:error, _} = Parser.parse("https://example.com/empty")
end
test "doesn't just add a title" do
assert {:error, {:invalid_metadata, _}} = Parser.parse("https://example.com/non-ogp")
end
test "parses ogp" do
assert Parser.parse("https://example.com/ogp") ==
{:ok,
%{
"image" => "http://ia.media-imdb.com/images/rock.jpg",
"title" => "The Rock",
"description" =>
"Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer.",
"type" => "video.movie",
"url" => "https://example.com/ogp"
}}
end
test "falls back to <title> when ogp:title is missing" do
assert Parser.parse("https://example.com/ogp-missing-title") ==
{:ok,
%{
"image" => "http://ia.media-imdb.com/images/rock.jpg",
"title" => "The Rock (1996)",
"description" =>
"Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer.",
"type" => "video.movie",
"url" => "https://example.com/ogp-missing-title"
}}
end
test "parses twitter card" do
assert Parser.parse("https://example.com/twitter-card") ==
{:ok,
%{
"card" => "summary",
"site" => "@flickr",
"image" => "https://farm6.staticflickr.com/5510/14338202952_93595258ff_z.jpg",
"title" => "Small Island Developing States Photo Submission",
"description" => "View the album on Flickr.",
"url" => "https://example.com/twitter-card"
}}
end
test "parses OEmbed and filters HTML tags" do
assert Parser.parse("https://example.com/oembed") ==
{:ok,
%{
"author_name" => "\u202E\u202D\u202Cbees\u202C",
"author_url" => "https://www.flickr.com/photos/bees/",
"cache_age" => 3600,
"flickr_type" => "photo",
"height" => "768",
"html" =>
"<a href=\"https://www.flickr.com/photos/bees/2362225867/\" title=\"Bacon Lollys by \u202E\u202D\u202Cbees\u202C, on Flickr\"><img src=\"https://farm4.staticflickr.com/3040/2362225867_4a87ab8baf_b.jpg\" width=\"1024\" height=\"768\" alt=\"Bacon Lollys\"/></a>",
"license" => "All Rights Reserved",
"license_id" => 0,
"provider_name" => "Flickr",
"provider_url" => "https://www.flickr.com/",
"thumbnail_height" => 150,
"thumbnail_url" =>
"https://farm4.staticflickr.com/3040/2362225867_4a87ab8baf_q.jpg",
"thumbnail_width" => 150,
"title" => "Bacon Lollys",
"type" => "photo",
"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",
"width" => "1024"
}}
end
test "rejects invalid OGP data" do
assert {:error, _} = Parser.parse("https://example.com/malformed")
end
test "returns error if getting page was not successful" do
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("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("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

File Metadata

Mime Type
text/x-diff
Expires
Thu, Oct 29, 10:18 AM (17 h, 51 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1797952
Default Alt Text
(747 KB)

Event Timeline