Page MenuHomePhorge

No OneTemporary

Authored By
Unknown
Size
288 KB
Referenced Files
None
Subscribers
None
This file is larger than 256 KB, so syntax highlighting was skipped.
diff --git a/config/config.exs b/config/config.exs
index ed37b93c0..0649f3078 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -1,803 +1,804 @@
# .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 Mix.Config module.
#
# This configuration file is loaded before any dependency and
# is restricted to this project.
use Mix.Config
# General application configuration
config :pleroma, ecto_repos: [Pleroma.Repo]
config :pleroma, Pleroma.Repo,
types: Pleroma.PostgresTypes,
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,
proxy_opts: [
redirect_on_failure: false,
max_body_length: 25 * 1_048_576,
http: [
follow_redirect: true,
pool: :upload
]
],
filename_display_max_length: 30,
default_description: nil
config :pleroma, Pleroma.Uploaders.Local, uploads: "uploads"
config :pleroma, Pleroma.Uploaders.S3,
bucket: nil,
streaming_enabled: true,
public_endpoint: "https://s3.amazonaws.com"
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"
]
websocket_config = [
path: "/websocket",
serializer: [
{Phoenix.Socket.V1.JSONSerializer, "~> 1.0.0"},
{Phoenix.Socket.V2.JSONSerializer, "~> 2.0.0"}
],
timeout: 60_000,
transport_log: false,
compress: false
]
# Configures the endpoint
config :pleroma, Pleroma.Web.Endpoint,
instrumenters: [Pleroma.Web.Endpoint.Instrumenter],
url: [host: "localhost"],
http: [
ip: {127, 0, 0, 1},
dispatch: [
{:_,
[
{"/api/v1/streaming", Pleroma.Web.MastodonAPI.WebsocketHandler, []},
{"/websocket", Phoenix.Endpoint.CowboyWebSocket,
{Phoenix.Transports.WebSocket,
{Pleroma.Web.Endpoint, Pleroma.Web.UserSocket, websocket_config}}},
{:_, Phoenix.Endpoint.Cowboy2Handler, {Pleroma.Web.Endpoint, []}}
]}
]
],
protocol: "https",
secret_key_base: "aK4Abxf29xU9TTDKre9coZPUgevcVCFQJe/5xP/7Lt4BEif6idBIbjupVbOrbKxl",
signing_salt: "CqaoopA2",
render_errors: [view: Pleroma.Web.ErrorView, accepts: ~w(json)],
pubsub: [name: Pleroma.PubSub, adapter: Phoenix.PubSub.PG2],
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 :quack,
level: :warn,
meta: [:all],
webhook_url: "https://hooks.slack.com/services/YOUR-KEY-HERE"
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",
background_image: "/images/city.jpg",
instance_thumbnail: "/instance/thumbnail.jpeg",
limit: 5_000,
description_limit: 5_000,
chat_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,
federation_publisher_modules: [
Pleroma.Web.ActivityPub.Publisher
],
allow_relay: true,
public: true,
quarantined_instances: [],
managed_config: true,
static_dir: "instance/static/",
allowed_post_formats: [
"text/plain",
"text/html",
"text/markdown",
"text/bbcode"
],
autofollowed_nicknames: [],
max_pinned_statuses: 1,
attachment_links: false,
max_report_comment_size: 1000,
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
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.png",
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
},
masto_fe: %{
showInstanceSpecificPanel: true
}
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.png",
type: "image/png"
}
],
theme_color: "#282c37",
background_color: "#191b22"
config :pleroma, :activitypub,
unfollow_blocked: true,
outgoing_blocks: 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_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, :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]
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,
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
config :pleroma, :chat, enabled: true
config :phoenix, :format_encoders, 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,
Pleroma.Web.Metadata.Providers.RelMe,
Pleroma.Web.Metadata.Providers.Feed
],
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,
federator_incoming: 50,
federator_outgoing: 50,
web_push: 50,
mailer: 10,
transmogrifier: 20,
scheduled_activities: 10,
background: 5,
remote_fetcher: 2,
attachments_cleanup: 5,
- new_users_digest: 1
+ new_users_digest: 1,
+ mute_expire: 5
],
plugins: [Oban.Plugins.Pruner],
crontab: [
{"0 0 * * *", Pleroma.Workers.Cron.ClearOauthTokenWorker},
{"0 * * * *", Pleroma.Workers.Cron.StatsWorker},
{"* * * * *", Pleroma.Workers.Cron.PurgeExpiredActivitiesWorker},
{"0 0 * * 0", Pleroma.Workers.Cron.DigestEmailsWorker},
{"0 0 * * *", Pleroma.Workers.Cron.NewUsersDigestWorker}
]
config :pleroma, :workers,
retries: [
federator_incoming: 5,
federator_outgoing: 5
]
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"
config :esshd,
enabled: false
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,
enforce_oauth_admin_scope_usage: true,
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 :prometheus, Pleroma.Web.Endpoint.MetricsExporter, path: "/api/pleroma/app_metrics"
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: 600,
issue_new_refresh_token: true,
clean_expired_tokens: false
config :pleroma, :database, rum_enabled: false
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.ActivityExpiration, enabled: true
config :pleroma, Pleroma.Plugs.RemoteIp, enabled: true
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,
+# 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",
"ref" => "master"
},
"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-fe" => %{
"name" => "soapbox-fe",
"git" => "https://gitlab.com/soapbox-pub/soapbox-fe",
"build_url" =>
"https://gitlab.com/soapbox-pub/soapbox-fe/-/jobs/artifacts/${ref}/download?job=build-production",
"ref" => "v1.0.0",
"build_dir" => "static"
}
}
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"],
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,
await_up_timeout: 5_000
config :pleroma, :pools,
federation: [
size: 50,
max_waiting: 10,
timeout: 10_000
],
media: [
size: 50,
max_waiting: 10,
timeout: 10_000
],
upload: [
size: 25,
max_waiting: 5,
timeout: 15_000
],
default: [
size: 10,
max_waiting: 2,
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
]
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,
transparency: true,
transparency_exclusions: []
config :tzdata, :http_client, Pleroma.HTTP.Tzdata
config :ex_aws, http_client: Pleroma.HTTP.ExAws
config :pleroma, :instances_favicons, enabled: false
config :floki, :html_parser, Floki.HTMLParser.FastHtml
config :pleroma, Pleroma.Web.Auth.Authenticator, Pleroma.Web.Auth.PleromaAuthenticator
# 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/lib/pleroma/user.ex b/lib/pleroma/user.ex
index 94c96de8d..040db8d80 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -1,2428 +1,2431 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.User do
use Ecto.Schema
import Ecto.Changeset
import Ecto.Query
import Ecto, only: [assoc: 2]
alias Ecto.Multi
alias Pleroma.Activity
alias Pleroma.Config
alias Pleroma.Conversation.Participation
alias Pleroma.Delivery
alias Pleroma.EctoType.ActivityPub.ObjectValidators
alias Pleroma.Emoji
alias Pleroma.FollowingRelationship
alias Pleroma.Formatter
alias Pleroma.HTML
alias Pleroma.Keys
alias Pleroma.MFA
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Registration
alias Pleroma.Repo
alias Pleroma.RepoStreamer
alias Pleroma.User
alias Pleroma.UserRelationship
alias Pleroma.Web
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
alias Pleroma.Web.OAuth
alias Pleroma.Web.RelMe
alias Pleroma.Workers.BackgroundWorker
require Logger
@type t :: %__MODULE__{}
@type account_status ::
:active
| :deactivated
| :password_reset_pending
| :confirmation_pending
| :approval_pending
@primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
# credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
@email_regex ~r/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/
@strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
@extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
# AP ID user relationships (blocks, mutes etc.)
# Format: [rel_type: [outgoing_rel: :outgoing_rel_target, incoming_rel: :incoming_rel_source]]
@user_relationships_config [
block: [
blocker_blocks: :blocked_users,
blockee_blocks: :blocker_users
],
mute: [
muter_mutes: :muted_users,
mutee_mutes: :muter_users
],
reblog_mute: [
reblog_muter_mutes: :reblog_muted_users,
reblog_mutee_mutes: :reblog_muter_users
],
notification_mute: [
notification_muter_mutes: :notification_muted_users,
notification_mutee_mutes: :notification_muter_users
],
# Note: `inverse_subscription` relationship is inverse: subscriber acts as relationship target
inverse_subscription: [
subscribee_subscriptions: :subscriber_users,
subscriber_subscriptions: :subscribee_users
]
]
schema "users" do
field(:bio, :string, default: "")
field(:raw_bio, :string)
field(:email, :string)
field(:name, :string)
field(:nickname, :string)
field(:password_hash, :string)
field(:password, :string, virtual: true)
field(:password_confirmation, :string, virtual: true)
field(:keys, :string)
field(:public_key, :string)
field(:ap_id, :string)
field(:avatar, :map, default: %{})
field(:local, :boolean, default: true)
field(:follower_address, :string)
field(:following_address, :string)
field(:search_rank, :float, virtual: true)
field(:search_type, :integer, virtual: true)
field(:tags, {:array, :string}, default: [])
field(:last_refreshed_at, :naive_datetime_usec)
field(:last_digest_emailed_at, :naive_datetime)
field(:banner, :map, default: %{})
field(:background, :map, default: %{})
field(:note_count, :integer, default: 0)
field(:follower_count, :integer, default: 0)
field(:following_count, :integer, default: 0)
field(:locked, :boolean, default: false)
field(:confirmation_pending, :boolean, default: false)
field(:password_reset_pending, :boolean, default: false)
field(:approval_pending, :boolean, default: false)
field(:registration_reason, :string, default: nil)
field(:confirmation_token, :string, default: nil)
field(:default_scope, :string, default: "public")
field(:domain_blocks, {:array, :string}, default: [])
field(:deactivated, :boolean, default: false)
field(:no_rich_text, :boolean, default: false)
field(:ap_enabled, :boolean, default: false)
field(:is_moderator, :boolean, default: false)
field(:is_admin, :boolean, default: false)
field(:show_role, :boolean, default: true)
field(:mastofe_settings, :map, default: nil)
field(:uri, ObjectValidators.Uri, default: nil)
field(:hide_followers_count, :boolean, default: false)
field(:hide_follows_count, :boolean, default: false)
field(:hide_followers, :boolean, default: false)
field(:hide_follows, :boolean, default: false)
field(:hide_favorites, :boolean, default: true)
field(:unread_conversation_count, :integer, default: 0)
field(:pinned_activities, {:array, :string}, default: [])
field(:email_notifications, :map, default: %{"digest" => false})
field(:mascot, :map, default: nil)
field(:emoji, :map, default: %{})
field(:pleroma_settings_store, :map, default: %{})
field(:fields, {:array, :map}, default: [])
field(:raw_fields, {:array, :map}, default: [])
field(:discoverable, :boolean, default: false)
field(:invisible, :boolean, default: false)
field(:allow_following_move, :boolean, default: true)
field(:skip_thread_containment, :boolean, default: false)
field(:actor_type, :string, default: "Person")
field(:also_known_as, {:array, :string}, default: [])
field(:inbox, :string)
field(:shared_inbox, :string)
field(:accepts_chat_messages, :boolean, default: nil)
embeds_one(
:notification_settings,
Pleroma.User.NotificationSetting,
on_replace: :update
)
has_many(:notifications, Notification)
has_many(:registrations, Registration)
has_many(:deliveries, Delivery)
has_many(:outgoing_relationships, UserRelationship, foreign_key: :source_id)
has_many(:incoming_relationships, UserRelationship, foreign_key: :target_id)
for {relationship_type,
[
{outgoing_relation, outgoing_relation_target},
{incoming_relation, incoming_relation_source}
]} <- @user_relationships_config do
# Definitions of `has_many` relations: :blocker_blocks, :muter_mutes, :reblog_muter_mutes,
# :notification_muter_mutes, :subscribee_subscriptions
has_many(outgoing_relation, UserRelationship,
foreign_key: :source_id,
where: [relationship_type: relationship_type]
)
# Definitions of `has_many` relations: :blockee_blocks, :mutee_mutes, :reblog_mutee_mutes,
# :notification_mutee_mutes, :subscriber_subscriptions
has_many(incoming_relation, UserRelationship,
foreign_key: :target_id,
where: [relationship_type: relationship_type]
)
# Definitions of `has_many` relations: :blocked_users, :muted_users, :reblog_muted_users,
# :notification_muted_users, :subscriber_users
has_many(outgoing_relation_target, through: [outgoing_relation, :target])
# Definitions of `has_many` relations: :blocker_users, :muter_users, :reblog_muter_users,
# :notification_muter_users, :subscribee_users
has_many(incoming_relation_source, through: [incoming_relation, :source])
end
# `:blocks` is deprecated (replaced with `blocked_users` relation)
field(:blocks, {:array, :string}, default: [])
# `:mutes` is deprecated (replaced with `muted_users` relation)
field(:mutes, {:array, :string}, default: [])
# `:muted_reblogs` is deprecated (replaced with `reblog_muted_users` relation)
field(:muted_reblogs, {:array, :string}, default: [])
# `:muted_notifications` is deprecated (replaced with `notification_muted_users` relation)
field(:muted_notifications, {:array, :string}, default: [])
# `:subscribers` is deprecated (replaced with `subscriber_users` relation)
field(:subscribers, {:array, :string}, default: [])
embeds_one(
:multi_factor_authentication_settings,
MFA.Settings,
on_replace: :delete
)
timestamps()
end
for {_relationship_type, [{_outgoing_relation, outgoing_relation_target}, _]} <-
@user_relationships_config do
# `def blocked_users_relation/2`, `def muted_users_relation/2`,
# `def reblog_muted_users_relation/2`, `def notification_muted_users/2`,
# `def subscriber_users/2`
def unquote(:"#{outgoing_relation_target}_relation")(user, restrict_deactivated? \\ false) do
target_users_query = assoc(user, unquote(outgoing_relation_target))
if restrict_deactivated? do
restrict_deactivated(target_users_query)
else
target_users_query
end
end
# `def blocked_users/2`, `def muted_users/2`, `def reblog_muted_users/2`,
# `def notification_muted_users/2`, `def subscriber_users/2`
def unquote(outgoing_relation_target)(user, restrict_deactivated? \\ false) do
__MODULE__
|> apply(unquote(:"#{outgoing_relation_target}_relation"), [
user,
restrict_deactivated?
])
|> Repo.all()
end
# `def blocked_users_ap_ids/2`, `def muted_users_ap_ids/2`, `def reblog_muted_users_ap_ids/2`,
# `def notification_muted_users_ap_ids/2`, `def subscriber_users_ap_ids/2`
def unquote(:"#{outgoing_relation_target}_ap_ids")(user, restrict_deactivated? \\ false) do
__MODULE__
|> apply(unquote(:"#{outgoing_relation_target}_relation"), [
user,
restrict_deactivated?
])
|> select([u], u.ap_id)
|> Repo.all()
end
end
defdelegate following_count(user), to: FollowingRelationship
defdelegate following(user), to: FollowingRelationship
defdelegate following?(follower, followed), to: FollowingRelationship
defdelegate following_ap_ids(user), to: FollowingRelationship
defdelegate get_follow_requests(user), to: FollowingRelationship
defdelegate search(query, opts \\ []), to: User.Search
@doc """
Dumps Flake Id to SQL-compatible format (16-byte UUID).
E.g. "9pQtDGXuq4p3VlcJEm" -> <<0, 0, 1, 110, 179, 218, 42, 92, 213, 41, 44, 227, 95, 213, 0, 0>>
"""
def binary_id(source_id) when is_binary(source_id) do
with {:ok, dumped_id} <- FlakeId.Ecto.CompatType.dump(source_id) do
dumped_id
else
_ -> source_id
end
end
def binary_id(source_ids) when is_list(source_ids) do
Enum.map(source_ids, &binary_id/1)
end
def binary_id(%User{} = user), do: binary_id(user.id)
@doc "Returns status account"
@spec account_status(User.t()) :: account_status()
def account_status(%User{deactivated: true}), do: :deactivated
def account_status(%User{password_reset_pending: true}), do: :password_reset_pending
def account_status(%User{approval_pending: true}), do: :approval_pending
def account_status(%User{confirmation_pending: true}) do
if Config.get([:instance, :account_activation_required]) do
:confirmation_pending
else
:active
end
end
def account_status(%User{}), do: :active
@spec visible_for(User.t(), User.t() | nil) ::
:visible
| :invisible
| :restricted_unauthenticated
| :deactivated
| :confirmation_pending
def visible_for(user, for_user \\ nil)
def visible_for(%User{invisible: true}, _), do: :invisible
def visible_for(%User{id: user_id}, %User{id: user_id}), do: :visible
def visible_for(%User{} = user, nil) do
if restrict_unauthenticated?(user) do
:restrict_unauthenticated
else
visible_account_status(user)
end
end
def visible_for(%User{} = user, for_user) do
if superuser?(for_user) do
:visible
else
visible_account_status(user)
end
end
def visible_for(_, _), do: :invisible
defp restrict_unauthenticated?(%User{local: true}) do
Config.restrict_unauthenticated_access?(:profiles, :local)
end
defp restrict_unauthenticated?(%User{local: _}) do
Config.restrict_unauthenticated_access?(:profiles, :remote)
end
defp visible_account_status(user) do
status = account_status(user)
if status in [:active, :password_reset_pending] do
:visible
else
status
end
end
@spec superuser?(User.t()) :: boolean()
def superuser?(%User{local: true, is_admin: true}), do: true
def superuser?(%User{local: true, is_moderator: true}), do: true
def superuser?(_), do: false
@spec invisible?(User.t()) :: boolean()
def invisible?(%User{invisible: true}), do: true
def invisible?(_), do: false
def avatar_url(user, options \\ []) do
case user.avatar do
%{"url" => [%{"href" => href} | _]} ->
href
_ ->
unless options[:no_default] do
Config.get([:assets, :default_user_avatar], "#{Web.base_url()}/images/avi.png")
end
end
end
def banner_url(user, options \\ []) do
case user.banner do
%{"url" => [%{"href" => href} | _]} -> href
_ -> !options[:no_default] && "#{Web.base_url()}/images/banner.png"
end
end
# Should probably be renamed or removed
def ap_id(%User{nickname: nickname}), do: "#{Web.base_url()}/users/#{nickname}"
def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
@spec ap_following(User.t()) :: String.t()
def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
@spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t()
def restrict_deactivated(query) do
from(u in query, where: u.deactivated != ^true)
end
defp truncate_fields_param(params) do
if Map.has_key?(params, :fields) do
Map.put(params, :fields, Enum.map(params[:fields], &truncate_field/1))
else
params
end
end
defp truncate_if_exists(params, key, max_length) do
if Map.has_key?(params, key) and is_binary(params[key]) do
{value, _chopped} = String.split_at(params[key], max_length)
Map.put(params, key, value)
else
params
end
end
defp fix_follower_address(%{follower_address: _, following_address: _} = params), do: params
defp fix_follower_address(%{nickname: nickname} = params),
do: Map.put(params, :follower_address, ap_followers(%User{nickname: nickname}))
defp fix_follower_address(params), do: params
def remote_user_changeset(struct \\ %User{local: false}, params) do
bio_limit = Config.get([:instance, :user_bio_length], 5000)
name_limit = Config.get([:instance, :user_name_length], 100)
name =
case params[:name] do
name when is_binary(name) and byte_size(name) > 0 -> name
_ -> params[:nickname]
end
params =
params
|> Map.put(:name, name)
|> Map.put_new(:last_refreshed_at, NaiveDateTime.utc_now())
|> truncate_if_exists(:name, name_limit)
|> truncate_if_exists(:bio, bio_limit)
|> truncate_fields_param()
|> fix_follower_address()
struct
|> cast(
params,
[
:bio,
:name,
:emoji,
:ap_id,
:inbox,
:shared_inbox,
:nickname,
:public_key,
:avatar,
:ap_enabled,
:banner,
:locked,
:last_refreshed_at,
:uri,
:follower_address,
:following_address,
:hide_followers,
:hide_follows,
:hide_followers_count,
:hide_follows_count,
:follower_count,
:fields,
:following_count,
:discoverable,
:invisible,
:actor_type,
:also_known_as,
:accepts_chat_messages
]
)
|> validate_required([:name, :ap_id])
|> unique_constraint(:nickname)
|> validate_format(:nickname, @email_regex)
|> validate_length(:bio, max: bio_limit)
|> validate_length(:name, max: name_limit)
|> validate_fields(true)
end
def update_changeset(struct, params \\ %{}) do
bio_limit = Config.get([:instance, :user_bio_length], 5000)
name_limit = Config.get([:instance, :user_name_length], 100)
struct
|> cast(
params,
[
:bio,
:raw_bio,
:name,
:emoji,
:avatar,
:public_key,
:inbox,
:shared_inbox,
:locked,
:no_rich_text,
:default_scope,
:banner,
:hide_follows,
:hide_followers,
:hide_followers_count,
:hide_follows_count,
:hide_favorites,
:allow_following_move,
:background,
:show_role,
:skip_thread_containment,
:fields,
:raw_fields,
:pleroma_settings_store,
:discoverable,
:actor_type,
:also_known_as,
:accepts_chat_messages
]
)
|> unique_constraint(:nickname)
|> validate_format(:nickname, local_nickname_regex())
|> validate_length(:bio, max: bio_limit)
|> validate_length(:name, min: 1, max: name_limit)
|> validate_inclusion(:actor_type, ["Person", "Service"])
|> put_fields()
|> put_emoji()
|> put_change_if_present(:bio, &{:ok, parse_bio(&1, struct)})
|> put_change_if_present(:avatar, &put_upload(&1, :avatar))
|> put_change_if_present(:banner, &put_upload(&1, :banner))
|> put_change_if_present(:background, &put_upload(&1, :background))
|> put_change_if_present(
:pleroma_settings_store,
&{:ok, Map.merge(struct.pleroma_settings_store, &1)}
)
|> validate_fields(false)
end
defp put_fields(changeset) do
if raw_fields = get_change(changeset, :raw_fields) do
raw_fields =
raw_fields
|> Enum.filter(fn %{"name" => n} -> n != "" end)
fields =
raw_fields
|> Enum.map(fn f -> Map.update!(f, "value", &parse_fields(&1)) end)
changeset
|> put_change(:raw_fields, raw_fields)
|> put_change(:fields, fields)
else
changeset
end
end
defp parse_fields(value) do
value
|> Formatter.linkify(mentions_format: :full)
|> elem(0)
end
defp put_emoji(changeset) do
emojified_fields = [:bio, :name, :raw_fields]
if Enum.any?(changeset.changes, fn {k, _} -> k in emojified_fields end) do
bio = Emoji.Formatter.get_emoji_map(get_field(changeset, :bio))
name = Emoji.Formatter.get_emoji_map(get_field(changeset, :name))
emoji = Map.merge(bio, name)
emoji =
changeset
|> get_field(:raw_fields)
|> Enum.reduce(emoji, fn x, acc ->
Map.merge(acc, Emoji.Formatter.get_emoji_map(x["name"] <> x["value"]))
end)
put_change(changeset, :emoji, emoji)
else
changeset
end
end
defp put_change_if_present(changeset, map_field, value_function) do
with {:ok, value} <- fetch_change(changeset, map_field),
{:ok, new_value} <- value_function.(value) do
put_change(changeset, map_field, new_value)
else
_ -> changeset
end
end
defp put_upload(value, type) do
with %Plug.Upload{} <- value,
{:ok, object} <- ActivityPub.upload(value, type: type) do
{:ok, object.data}
end
end
def update_as_admin_changeset(struct, params) do
struct
|> update_changeset(params)
|> cast(params, [:email])
|> delete_change(:also_known_as)
|> unique_constraint(:email)
|> validate_format(:email, @email_regex)
|> validate_inclusion(:actor_type, ["Person", "Service"])
end
@spec update_as_admin(User.t(), map()) :: {:ok, User.t()} | {:error, Changeset.t()}
def update_as_admin(user, params) do
params = Map.put(params, "password_confirmation", params["password"])
changeset = update_as_admin_changeset(user, params)
if params["password"] do
reset_password(user, changeset, params)
else
User.update_and_set_cache(changeset)
end
end
def password_update_changeset(struct, params) do
struct
|> cast(params, [:password, :password_confirmation])
|> validate_required([:password, :password_confirmation])
|> validate_confirmation(:password)
|> put_password_hash()
|> put_change(:password_reset_pending, false)
end
@spec reset_password(User.t(), map()) :: {:ok, User.t()} | {:error, Changeset.t()}
def reset_password(%User{} = user, params) do
reset_password(user, user, params)
end
def reset_password(%User{id: user_id} = user, struct, params) do
multi =
Multi.new()
|> Multi.update(:user, password_update_changeset(struct, params))
|> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
|> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
case Repo.transaction(multi) do
{:ok, %{user: user} = _} -> set_cache(user)
{:error, _, changeset, _} -> {:error, changeset}
end
end
def update_password_reset_pending(user, value) do
user
|> change()
|> put_change(:password_reset_pending, value)
|> update_and_set_cache()
end
def force_password_reset_async(user) do
BackgroundWorker.enqueue("force_password_reset", %{"user_id" => user.id})
end
@spec force_password_reset(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
def force_password_reset(user), do: update_password_reset_pending(user, true)
# Used to auto-register LDAP accounts which won't have a password hash stored locally
def register_changeset_ldap(struct, params = %{password: password})
when is_nil(password) do
params = Map.put_new(params, :accepts_chat_messages, true)
params =
if Map.has_key?(params, :email) do
Map.put_new(params, :email, params[:email])
else
params
end
struct
|> cast(params, [
:name,
:nickname,
:email,
:accepts_chat_messages
])
|> validate_required([:name, :nickname])
|> unique_constraint(:nickname)
|> validate_exclusion(:nickname, Config.get([User, :restricted_nicknames]))
|> validate_format(:nickname, local_nickname_regex())
|> put_ap_id()
|> unique_constraint(:ap_id)
|> put_following_and_follower_address()
end
def register_changeset(struct, params \\ %{}, opts \\ []) do
bio_limit = Config.get([:instance, :user_bio_length], 5000)
name_limit = Config.get([:instance, :user_name_length], 100)
reason_limit = Config.get([:instance, :registration_reason_length], 500)
params = Map.put_new(params, :accepts_chat_messages, true)
need_confirmation? =
if is_nil(opts[:need_confirmation]) do
Config.get([:instance, :account_activation_required])
else
opts[:need_confirmation]
end
need_approval? =
if is_nil(opts[:need_approval]) do
Config.get([:instance, :account_approval_required])
else
opts[:need_approval]
end
struct
|> confirmation_changeset(need_confirmation: need_confirmation?)
|> approval_changeset(need_approval: need_approval?)
|> cast(params, [
:bio,
:raw_bio,
:email,
:name,
:nickname,
:password,
:password_confirmation,
:emoji,
:accepts_chat_messages,
:registration_reason
])
|> validate_required([:name, :nickname, :password, :password_confirmation])
|> validate_confirmation(:password)
|> unique_constraint(:email)
|> validate_format(:email, @email_regex)
|> validate_change(:email, fn :email, email ->
valid? =
Config.get([User, :email_blacklist])
|> Enum.all?(fn blacklisted_domain ->
!String.ends_with?(email, ["@" <> blacklisted_domain, "." <> blacklisted_domain])
end)
if valid?, do: [], else: [email: "Invalid email"]
end)
|> unique_constraint(:nickname)
|> validate_exclusion(:nickname, Config.get([User, :restricted_nicknames]))
|> validate_format(:nickname, local_nickname_regex())
|> validate_length(:bio, max: bio_limit)
|> validate_length(:name, min: 1, max: name_limit)
|> validate_length(:registration_reason, max: reason_limit)
|> maybe_validate_required_email(opts[:external])
|> put_password_hash
|> put_ap_id()
|> unique_constraint(:ap_id)
|> put_following_and_follower_address()
end
def maybe_validate_required_email(changeset, true), do: changeset
def maybe_validate_required_email(changeset, _) do
if Config.get([:instance, :account_activation_required]) do
validate_required(changeset, [:email])
else
changeset
end
end
defp put_ap_id(changeset) do
ap_id = ap_id(%User{nickname: get_field(changeset, :nickname)})
put_change(changeset, :ap_id, ap_id)
end
defp put_following_and_follower_address(changeset) do
followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})
changeset
|> put_change(:follower_address, followers)
end
defp autofollow_users(user) do
candidates = Config.get([:instance, :autofollowed_nicknames])
autofollowed_users =
User.Query.build(%{nickname: candidates, local: true, deactivated: false})
|> Repo.all()
follow_all(user, autofollowed_users)
end
@doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
def register(%Ecto.Changeset{} = changeset) do
with {:ok, user} <- Repo.insert(changeset) do
post_register_action(user)
end
end
def post_register_action(%User{} = user) do
with {:ok, user} <- autofollow_users(user),
{:ok, user} <- set_cache(user),
{:ok, _} <- send_welcome_email(user),
{:ok, _} <- send_welcome_message(user),
{:ok, _} <- send_welcome_chat_message(user),
{:ok, _} <- try_send_confirmation_email(user) do
{:ok, user}
end
end
def send_welcome_message(user) do
if User.WelcomeMessage.enabled?() do
User.WelcomeMessage.post_message(user)
{:ok, :enqueued}
else
{:ok, :noop}
end
end
def send_welcome_chat_message(user) do
if User.WelcomeChatMessage.enabled?() do
User.WelcomeChatMessage.post_message(user)
{:ok, :enqueued}
else
{:ok, :noop}
end
end
def send_welcome_email(%User{email: email} = user) when is_binary(email) do
if User.WelcomeEmail.enabled?() do
User.WelcomeEmail.send_email(user)
{:ok, :enqueued}
else
{:ok, :noop}
end
end
def send_welcome_email(_), do: {:ok, :noop}
@spec try_send_confirmation_email(User.t()) :: {:ok, :enqueued | :noop}
def try_send_confirmation_email(%User{confirmation_pending: true} = user) do
if Config.get([:instance, :account_activation_required]) do
send_confirmation_email(user)
{:ok, :enqueued}
else
{:ok, :noop}
end
end
def try_send_confirmation_email(_), do: {:ok, :noop}
@spec send_confirmation_email(Uset.t()) :: User.t()
def send_confirmation_email(%User{} = user) do
user
|> Pleroma.Emails.UserEmail.account_confirmation_email()
|> Pleroma.Emails.Mailer.deliver_async()
user
end
def needs_update?(%User{local: true}), do: false
def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
def needs_update?(%User{local: false} = user) do
NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
end
def needs_update?(_), do: true
@spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
# "Locked" (self-locked) users demand explicit authorization of follow requests
def maybe_direct_follow(%User{} = follower, %User{local: true, locked: true} = followed) do
follow(follower, followed, :follow_pending)
end
def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
follow(follower, followed)
end
def maybe_direct_follow(%User{} = follower, %User{} = followed) do
if not ap_enabled?(followed) do
follow(follower, followed)
else
{:ok, follower}
end
end
@doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
@spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
def follow_all(follower, followeds) do
followeds
|> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
|> Enum.each(&follow(follower, &1, :follow_accept))
set_cache(follower)
end
def follow(%User{} = follower, %User{} = followed, state \\ :follow_accept) do
deny_follow_blocked = Config.get([:user, :deny_follow_blocked])
cond do
followed.deactivated ->
{:error, "Could not follow user: #{followed.nickname} is deactivated."}
deny_follow_blocked and blocks?(followed, follower) ->
{:error, "Could not follow user: #{followed.nickname} blocked you."}
true ->
FollowingRelationship.follow(follower, followed, state)
{:ok, _} = update_follower_count(followed)
follower
|> update_following_count()
end
end
def unfollow(%User{ap_id: ap_id}, %User{ap_id: ap_id}) do
{:error, "Not subscribed!"}
end
@spec unfollow(User.t(), User.t()) :: {:ok, User.t(), Activity.t()} | {:error, String.t()}
def unfollow(%User{} = follower, %User{} = followed) do
case do_unfollow(follower, followed) do
{:ok, follower, followed} ->
{:ok, follower, Utils.fetch_latest_follow(follower, followed)}
error ->
error
end
end
@spec do_unfollow(User.t(), User.t()) :: {:ok, User.t(), User.t()} | {:error, String.t()}
defp do_unfollow(%User{} = follower, %User{} = followed) do
case get_follow_state(follower, followed) do
state when state in [:follow_pending, :follow_accept] ->
FollowingRelationship.unfollow(follower, followed)
{:ok, followed} = update_follower_count(followed)
{:ok, follower} =
follower
|> update_following_count()
{:ok, follower, followed}
nil ->
{:error, "Not subscribed!"}
end
end
@doc "Returns follow state as Pleroma.FollowingRelationship.State value"
def get_follow_state(%User{} = follower, %User{} = following) do
following_relationship = FollowingRelationship.get(follower, following)
get_follow_state(follower, following, following_relationship)
end
def get_follow_state(
%User{} = follower,
%User{} = following,
following_relationship
) do
case {following_relationship, following.local} do
{nil, false} ->
case Utils.fetch_latest_follow(follower, following) do
%Activity{data: %{"state" => state}} when state in ["pending", "accept"] ->
FollowingRelationship.state_to_enum(state)
_ ->
nil
end
{%{state: state}, _} ->
state
{nil, _} ->
nil
end
end
def locked?(%User{} = user) do
user.locked || false
end
def get_by_id(id) do
Repo.get_by(User, id: id)
end
def get_by_ap_id(ap_id) do
Repo.get_by(User, ap_id: ap_id)
end
def get_all_by_ap_id(ap_ids) do
from(u in __MODULE__,
where: u.ap_id in ^ap_ids
)
|> Repo.all()
end
def get_all_by_ids(ids) do
from(u in __MODULE__, where: u.id in ^ids)
|> Repo.all()
end
# This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
# of the ap_id and the domain and tries to get that user
def get_by_guessed_nickname(ap_id) do
domain = URI.parse(ap_id).host
name = List.last(String.split(ap_id, "/"))
nickname = "#{name}@#{domain}"
get_cached_by_nickname(nickname)
end
def set_cache({:ok, user}), do: set_cache(user)
def set_cache({:error, err}), do: {:error, err}
def set_cache(%User{} = user) do
Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
Cachex.put(:user_cache, "friends_ap_ids:#{user.nickname}", get_user_friends_ap_ids(user))
{:ok, user}
end
def update_and_set_cache(struct, params) do
struct
|> update_changeset(params)
|> update_and_set_cache()
end
def update_and_set_cache(changeset) do
with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
set_cache(user)
end
end
def get_user_friends_ap_ids(user) do
from(u in User.get_friends_query(user), select: u.ap_id)
|> Repo.all()
end
@spec get_cached_user_friends_ap_ids(User.t()) :: [String.t()]
def get_cached_user_friends_ap_ids(user) do
Cachex.fetch!(:user_cache, "friends_ap_ids:#{user.ap_id}", fn _ ->
get_user_friends_ap_ids(user)
end)
end
def invalidate_cache(user) do
Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
Cachex.del(:user_cache, "nickname:#{user.nickname}")
Cachex.del(:user_cache, "friends_ap_ids:#{user.ap_id}")
end
@spec get_cached_by_ap_id(String.t()) :: User.t() | nil
def get_cached_by_ap_id(ap_id) do
key = "ap_id:#{ap_id}"
with {:ok, nil} <- Cachex.get(:user_cache, key),
user when not is_nil(user) <- get_by_ap_id(ap_id),
{:ok, true} <- Cachex.put(:user_cache, key, user) do
user
else
{:ok, user} -> user
nil -> nil
end
end
def get_cached_by_id(id) do
key = "id:#{id}"
ap_id =
Cachex.fetch!(:user_cache, key, fn _ ->
user = get_by_id(id)
if user do
Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
{:commit, user.ap_id}
else
{:ignore, ""}
end
end)
get_cached_by_ap_id(ap_id)
end
def get_cached_by_nickname(nickname) do
key = "nickname:#{nickname}"
Cachex.fetch!(:user_cache, key, fn ->
case get_or_fetch_by_nickname(nickname) do
{:ok, user} -> {:commit, user}
{:error, _error} -> {:ignore, nil}
end
end)
end
def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
restrict_to_local = Config.get([:instance, :limit_to_local_content])
cond do
is_integer(nickname_or_id) or FlakeId.flake_id?(nickname_or_id) ->
get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
restrict_to_local == false or not String.contains?(nickname_or_id, "@") ->
get_cached_by_nickname(nickname_or_id)
restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
get_cached_by_nickname(nickname_or_id)
true ->
nil
end
end
@spec get_by_nickname(String.t()) :: User.t() | nil
def get_by_nickname(nickname) do
Repo.get_by(User, nickname: nickname) ||
if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
Repo.get_by(User, nickname: local_nickname(nickname))
end
end
def get_by_email(email), do: Repo.get_by(User, email: email)
def get_by_nickname_or_email(nickname_or_email) do
get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
end
def fetch_by_nickname(nickname), do: ActivityPub.make_user_from_nickname(nickname)
def get_or_fetch_by_nickname(nickname) do
with %User{} = user <- get_by_nickname(nickname) do
{:ok, user}
else
_e ->
with [_nick, _domain] <- String.split(nickname, "@"),
{:ok, user} <- fetch_by_nickname(nickname) do
{:ok, user}
else
_e -> {:error, "not found " <> nickname}
end
end
end
@spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
def get_followers_query(%User{} = user, nil) do
User.Query.build(%{followers: user, deactivated: false})
end
def get_followers_query(user, page) do
user
|> get_followers_query(nil)
|> User.Query.paginate(page, 20)
end
@spec get_followers_query(User.t()) :: Ecto.Query.t()
def get_followers_query(user), do: get_followers_query(user, nil)
@spec get_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
def get_followers(user, page \\ nil) do
user
|> get_followers_query(page)
|> Repo.all()
end
@spec get_external_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
def get_external_followers(user, page \\ nil) do
user
|> get_followers_query(page)
|> User.Query.build(%{external: true})
|> Repo.all()
end
def get_followers_ids(user, page \\ nil) do
user
|> get_followers_query(page)
|> select([u], u.id)
|> Repo.all()
end
@spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
def get_friends_query(%User{} = user, nil) do
User.Query.build(%{friends: user, deactivated: false})
end
def get_friends_query(user, page) do
user
|> get_friends_query(nil)
|> User.Query.paginate(page, 20)
end
@spec get_friends_query(User.t()) :: Ecto.Query.t()
def get_friends_query(user), do: get_friends_query(user, nil)
def get_friends(user, page \\ nil) do
user
|> get_friends_query(page)
|> Repo.all()
end
def get_friends_ap_ids(user) do
user
|> get_friends_query(nil)
|> select([u], u.ap_id)
|> Repo.all()
end
def get_friends_ids(user, page \\ nil) do
user
|> get_friends_query(page)
|> select([u], u.id)
|> Repo.all()
end
def increase_note_count(%User{} = user) do
User
|> where(id: ^user.id)
|> update([u], inc: [note_count: 1])
|> select([u], u)
|> Repo.update_all([])
|> case do
{1, [user]} -> set_cache(user)
_ -> {:error, user}
end
end
def decrease_note_count(%User{} = user) do
User
|> where(id: ^user.id)
|> update([u],
set: [
note_count: fragment("greatest(0, note_count - 1)")
]
)
|> select([u], u)
|> Repo.update_all([])
|> case do
{1, [user]} -> set_cache(user)
_ -> {:error, user}
end
end
def update_note_count(%User{} = user, note_count \\ nil) do
note_count =
note_count ||
from(
a in Object,
where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
select: count(a.id)
)
|> Repo.one()
user
|> cast(%{note_count: note_count}, [:note_count])
|> update_and_set_cache()
end
@spec maybe_fetch_follow_information(User.t()) :: User.t()
def maybe_fetch_follow_information(user) do
with {:ok, user} <- fetch_follow_information(user) do
user
else
e ->
Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
user
end
end
def fetch_follow_information(user) do
with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
user
|> follow_information_changeset(info)
|> update_and_set_cache()
end
end
defp follow_information_changeset(user, params) do
user
|> cast(params, [
:hide_followers,
:hide_follows,
:follower_count,
:following_count,
:hide_followers_count,
:hide_follows_count
])
end
@spec update_follower_count(User.t()) :: {:ok, User.t()}
def update_follower_count(%User{} = user) do
if user.local or !Config.get([:instance, :external_user_synchronization]) do
follower_count = FollowingRelationship.follower_count(user)
user
|> follow_information_changeset(%{follower_count: follower_count})
|> update_and_set_cache
else
{:ok, maybe_fetch_follow_information(user)}
end
end
@spec update_following_count(User.t()) :: {:ok, User.t()}
def update_following_count(%User{local: false} = user) do
if Config.get([:instance, :external_user_synchronization]) do
{:ok, maybe_fetch_follow_information(user)}
else
{:ok, user}
end
end
def update_following_count(%User{local: true} = user) do
following_count = FollowingRelationship.following_count(user)
user
|> follow_information_changeset(%{following_count: following_count})
|> update_and_set_cache()
end
def set_unread_conversation_count(%User{local: true} = user) do
unread_query = Participation.unread_conversation_count_for_user(user)
User
|> join(:inner, [u], p in subquery(unread_query))
|> update([u, p],
set: [unread_conversation_count: p.count]
)
|> where([u], u.id == ^user.id)
|> select([u], u)
|> Repo.update_all([])
|> case do
{1, [user]} -> set_cache(user)
_ -> {:error, user}
end
end
def set_unread_conversation_count(user), do: {:ok, user}
def increment_unread_conversation_count(conversation, %User{local: true} = user) do
unread_query =
Participation.unread_conversation_count_for_user(user)
|> where([p], p.conversation_id == ^conversation.id)
User
|> join(:inner, [u], p in subquery(unread_query))
|> update([u, p],
inc: [unread_conversation_count: 1]
)
|> where([u], u.id == ^user.id)
|> where([u, p], p.count == 0)
|> select([u], u)
|> Repo.update_all([])
|> case do
{1, [user]} -> set_cache(user)
_ -> {:error, user}
end
end
def increment_unread_conversation_count(_, user), do: {:ok, user}
@spec get_users_from_set([String.t()], keyword()) :: [User.t()]
def get_users_from_set(ap_ids, opts \\ []) do
local_only = Keyword.get(opts, :local_only, true)
criteria = %{ap_id: ap_ids, deactivated: false}
criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
User.Query.build(criteria)
|> Repo.all()
end
@spec get_recipients_from_activity(Activity.t()) :: [User.t()]
def get_recipients_from_activity(%Activity{recipients: to, actor: actor}) do
to = [actor | to]
query = User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
query
|> Repo.all()
end
- @spec mute(User.t(), User.t(), boolean()) ::
+ @spec mute(User.t(), User.t(), map()) ::
{:ok, list(UserRelationship.t())} | {:error, String.t()}
- def mute(%User{} = muter, %User{} = mutee, notifications? \\ true) do
- add_to_mutes(muter, mutee, notifications?)
+ def mute(%User{} = muter, %User{} = mutee, params \\ %{}) do
+ notifications? = Map.get(params, :notifications, true)
+ expires_in = Map.get(params, :expires_in, 0)
+
+ with {:ok, user_mute} <- UserRelationship.create_mute(muter, mutee),
+ {:ok, user_notification_mute} <-
+ (notifications? && UserRelationship.create_notification_mute(muter, mutee)) ||
+ {:ok, nil} do
+ with seconds when seconds > 0 <- expires_in do
+ Pleroma.Workers.MuteExpireWorker.enqueue(
+ "unmute",
+ %{"muter" => muter.id, "mutee" => mutee.id},
+ schedule_in: expires_in
+ )
+ end
+
+ {:ok, Enum.filter([user_mute, user_notification_mute], & &1)}
+ end
end
def unmute(%User{} = muter, %User{} = mutee) do
- remove_from_mutes(muter, mutee)
+ with {:ok, user_mute} <- UserRelationship.delete_mute(muter, mutee),
+ {:ok, user_notification_mute} <-
+ UserRelationship.delete_notification_mute(muter, mutee) do
+ {:ok, [user_mute, user_notification_mute]}
+ end
end
def subscribe(%User{} = subscriber, %User{} = target) do
deny_follow_blocked = Config.get([:user, :deny_follow_blocked])
if blocks?(target, subscriber) and deny_follow_blocked do
{:error, "Could not subscribe: #{target.nickname} is blocking you"}
else
# Note: the relationship is inverse: subscriber acts as relationship target
UserRelationship.create_inverse_subscription(target, subscriber)
end
end
def subscribe(%User{} = subscriber, %{ap_id: ap_id}) do
with %User{} = subscribee <- get_cached_by_ap_id(ap_id) do
subscribe(subscriber, subscribee)
end
end
def unsubscribe(%User{} = unsubscriber, %User{} = target) do
# Note: the relationship is inverse: subscriber acts as relationship target
UserRelationship.delete_inverse_subscription(target, unsubscriber)
end
def unsubscribe(%User{} = unsubscriber, %{ap_id: ap_id}) do
with %User{} = user <- get_cached_by_ap_id(ap_id) do
unsubscribe(unsubscriber, user)
end
end
def block(%User{} = blocker, %User{} = blocked) do
# sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
blocker =
if following?(blocker, blocked) do
{:ok, blocker, _} = unfollow(blocker, blocked)
blocker
else
blocker
end
# clear any requested follows as well
blocked =
case CommonAPI.reject_follow_request(blocked, blocker) do
{:ok, %User{} = updated_blocked} -> updated_blocked
nil -> blocked
end
unsubscribe(blocked, blocker)
unfollowing_blocked = Config.get([:activitypub, :unfollow_blocked], true)
if unfollowing_blocked && following?(blocked, blocker), do: unfollow(blocked, blocker)
{:ok, blocker} = update_follower_count(blocker)
{:ok, blocker, _} = Participation.mark_all_as_read(blocker, blocked)
add_to_block(blocker, blocked)
end
# helper to handle the block given only an actor's AP id
def block(%User{} = blocker, %{ap_id: ap_id}) do
block(blocker, get_cached_by_ap_id(ap_id))
end
def unblock(%User{} = blocker, %User{} = blocked) do
remove_from_block(blocker, blocked)
end
# helper to handle the block given only an actor's AP id
def unblock(%User{} = blocker, %{ap_id: ap_id}) do
unblock(blocker, get_cached_by_ap_id(ap_id))
end
def mutes?(nil, _), do: false
def mutes?(%User{} = user, %User{} = target), do: mutes_user?(user, target)
def mutes_user?(%User{} = user, %User{} = target) do
UserRelationship.mute_exists?(user, target)
end
@spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
def muted_notifications?(nil, _), do: false
def muted_notifications?(%User{} = user, %User{} = target),
do: UserRelationship.notification_mute_exists?(user, target)
def blocks?(nil, _), do: false
def blocks?(%User{} = user, %User{} = target) do
blocks_user?(user, target) ||
(blocks_domain?(user, target) and not User.following?(user, target))
end
def blocks_user?(%User{} = user, %User{} = target) do
UserRelationship.block_exists?(user, target)
end
def blocks_user?(_, _), do: false
def blocks_domain?(%User{} = user, %User{} = target) do
domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks)
%{host: host} = URI.parse(target.ap_id)
Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
end
def blocks_domain?(_, _), do: false
def subscribed_to?(%User{} = user, %User{} = target) do
# Note: the relationship is inverse: subscriber acts as relationship target
UserRelationship.inverse_subscription_exists?(target, user)
end
def subscribed_to?(%User{} = user, %{ap_id: ap_id}) do
with %User{} = target <- get_cached_by_ap_id(ap_id) do
subscribed_to?(user, target)
end
end
@doc """
Returns map of outgoing (blocked, muted etc.) relationships' user AP IDs by relation type.
E.g. `outgoing_relationships_ap_ids(user, [:block])` -> `%{block: ["https://some.site/users/userapid"]}`
"""
@spec outgoing_relationships_ap_ids(User.t(), list(atom())) :: %{atom() => list(String.t())}
def outgoing_relationships_ap_ids(_user, []), do: %{}
def outgoing_relationships_ap_ids(nil, _relationship_types), do: %{}
def outgoing_relationships_ap_ids(%User{} = user, relationship_types)
when is_list(relationship_types) do
db_result =
user
|> assoc(:outgoing_relationships)
|> join(:inner, [user_rel], u in assoc(user_rel, :target))
|> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
|> select([user_rel, u], [user_rel.relationship_type, fragment("array_agg(?)", u.ap_id)])
|> group_by([user_rel, u], user_rel.relationship_type)
|> Repo.all()
|> Enum.into(%{}, fn [k, v] -> {k, v} end)
Enum.into(
relationship_types,
%{},
fn rel_type -> {rel_type, db_result[rel_type] || []} end
)
end
def incoming_relationships_ungrouped_ap_ids(user, relationship_types, ap_ids \\ nil)
def incoming_relationships_ungrouped_ap_ids(_user, [], _ap_ids), do: []
def incoming_relationships_ungrouped_ap_ids(nil, _relationship_types, _ap_ids), do: []
def incoming_relationships_ungrouped_ap_ids(%User{} = user, relationship_types, ap_ids)
when is_list(relationship_types) do
user
|> assoc(:incoming_relationships)
|> join(:inner, [user_rel], u in assoc(user_rel, :source))
|> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
|> maybe_filter_on_ap_id(ap_ids)
|> select([user_rel, u], u.ap_id)
|> distinct(true)
|> Repo.all()
end
defp maybe_filter_on_ap_id(query, ap_ids) when is_list(ap_ids) do
where(query, [user_rel, u], u.ap_id in ^ap_ids)
end
defp maybe_filter_on_ap_id(query, _ap_ids), do: query
def deactivate_async(user, status \\ true) do
BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
end
def deactivate(user, status \\ true)
def deactivate(users, status) when is_list(users) do
Repo.transaction(fn ->
for user <- users, do: deactivate(user, status)
end)
end
def deactivate(%User{} = user, status) do
with {:ok, user} <- set_activation_status(user, status) do
user
|> get_followers()
|> Enum.filter(& &1.local)
|> Enum.each(&set_cache(update_following_count(&1)))
# Only update local user counts, remote will be update during the next pull.
user
|> get_friends()
|> Enum.filter(& &1.local)
|> Enum.each(&do_unfollow(user, &1))
{:ok, user}
end
end
def approve(users) when is_list(users) do
Repo.transaction(fn ->
Enum.map(users, fn user ->
with {:ok, user} <- approve(user), do: user
end)
end)
end
def approve(%User{} = user) do
change(user, approval_pending: false)
|> update_and_set_cache()
end
def update_notification_settings(%User{} = user, settings) do
user
|> cast(%{notification_settings: settings}, [])
|> cast_embed(:notification_settings)
|> validate_required([:notification_settings])
|> update_and_set_cache()
end
@spec purge_user_changeset(User.t()) :: Changeset.t()
def purge_user_changeset(user) do
# "Right to be forgotten"
# https://gdpr.eu/right-to-be-forgotten/
change(user, %{
bio: "",
raw_bio: nil,
email: nil,
name: nil,
password_hash: nil,
keys: nil,
public_key: nil,
avatar: %{},
tags: [],
last_refreshed_at: nil,
last_digest_emailed_at: nil,
banner: %{},
background: %{},
note_count: 0,
follower_count: 0,
following_count: 0,
locked: false,
confirmation_pending: false,
password_reset_pending: false,
approval_pending: false,
registration_reason: nil,
confirmation_token: nil,
domain_blocks: [],
deactivated: true,
ap_enabled: false,
is_moderator: false,
is_admin: false,
mastofe_settings: nil,
mascot: nil,
emoji: %{},
pleroma_settings_store: %{},
fields: [],
raw_fields: [],
discoverable: false,
also_known_as: []
})
end
def delete(users) when is_list(users) do
for user <- users, do: delete(user)
end
def delete(%User{} = user) do
BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
end
defp delete_and_invalidate_cache(%User{} = user) do
invalidate_cache(user)
Repo.delete(user)
end
defp delete_or_deactivate(%User{local: false} = user), do: delete_and_invalidate_cache(user)
defp delete_or_deactivate(%User{local: true} = user) do
status = account_status(user)
case status do
:confirmation_pending ->
delete_and_invalidate_cache(user)
:approval_pending ->
delete_and_invalidate_cache(user)
_ ->
user
|> purge_user_changeset()
|> update_and_set_cache()
end
end
def perform(:force_password_reset, user), do: force_password_reset(user)
@spec perform(atom(), User.t()) :: {:ok, User.t()}
def perform(:delete, %User{} = user) do
# Remove all relationships
user
|> get_followers()
|> Enum.each(fn follower ->
ActivityPub.unfollow(follower, user)
unfollow(follower, user)
end)
user
|> get_friends()
|> Enum.each(fn followed ->
ActivityPub.unfollow(user, followed)
unfollow(user, followed)
end)
delete_user_activities(user)
delete_notifications_from_user_activities(user)
delete_outgoing_pending_follow_requests(user)
delete_or_deactivate(user)
end
def perform(:deactivate_async, user, status), do: deactivate(user, status)
@spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
when is_list(blocked_identifiers) do
Enum.map(
blocked_identifiers,
fn blocked_identifier ->
with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
{:ok, _block} <- CommonAPI.block(blocker, blocked) do
blocked
else
err ->
Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
err
end
end
)
end
def perform(:follow_import, %User{} = follower, followed_identifiers)
when is_list(followed_identifiers) do
Enum.map(
followed_identifiers,
fn followed_identifier ->
with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
{:ok, follower} <- maybe_direct_follow(follower, followed),
{:ok, _, _, _} <- CommonAPI.follow(follower, followed) do
followed
else
err ->
Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
err
end
end
)
end
@spec external_users_query() :: Ecto.Query.t()
def external_users_query do
User.Query.build(%{
external: true,
active: true,
order_by: :id
})
end
@spec external_users(keyword()) :: [User.t()]
def external_users(opts \\ []) do
query =
external_users_query()
|> select([u], struct(u, [:id, :ap_id]))
query =
if opts[:max_id],
do: where(query, [u], u.id > ^opts[:max_id]),
else: query
query =
if opts[:limit],
do: limit(query, ^opts[:limit]),
else: query
Repo.all(query)
end
def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
BackgroundWorker.enqueue("blocks_import", %{
"blocker_id" => blocker.id,
"blocked_identifiers" => blocked_identifiers
})
end
def follow_import(%User{} = follower, followed_identifiers)
when is_list(followed_identifiers) do
BackgroundWorker.enqueue("follow_import", %{
"follower_id" => follower.id,
"followed_identifiers" => followed_identifiers
})
end
def delete_notifications_from_user_activities(%User{ap_id: ap_id}) do
Notification
|> join(:inner, [n], activity in assoc(n, :activity))
|> where([n, a], fragment("? = ?", a.actor, ^ap_id))
|> Repo.delete_all()
end
def delete_user_activities(%User{ap_id: ap_id} = user) do
ap_id
|> Activity.Queries.by_actor()
|> RepoStreamer.chunk_stream(50)
|> Stream.each(fn activities ->
Enum.each(activities, fn activity -> delete_activity(activity, user) end)
end)
|> Stream.run()
end
defp delete_activity(%{data: %{"type" => "Create", "object" => object}} = activity, user) do
with {_, %Object{}} <- {:find_object, Object.get_by_ap_id(object)},
{:ok, delete_data, _} <- Builder.delete(user, object) do
Pipeline.common_pipeline(delete_data, local: user.local)
else
{:find_object, nil} ->
# We have the create activity, but not the object, it was probably pruned.
# Insert a tombstone and try again
with {:ok, tombstone_data, _} <- Builder.tombstone(user.ap_id, object),
{:ok, _tombstone} <- Object.create(tombstone_data) do
delete_activity(activity, user)
end
e ->
Logger.error("Could not delete #{object} created by #{activity.data["ap_id"]}")
Logger.error("Error: #{inspect(e)}")
end
end
defp delete_activity(%{data: %{"type" => type}} = activity, user)
when type in ["Like", "Announce"] do
{:ok, undo, _} = Builder.undo(user, activity)
Pipeline.common_pipeline(undo, local: user.local)
end
defp delete_activity(_activity, _user), do: "Doing nothing"
defp delete_outgoing_pending_follow_requests(user) do
user
|> FollowingRelationship.outgoing_pending_follow_requests_query()
|> Repo.delete_all()
end
def html_filter_policy(%User{no_rich_text: true}) do
Pleroma.HTML.Scrubber.TwitterText
end
def html_filter_policy(_), do: Config.get([:markup, :scrub_policy])
def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id)
def get_or_fetch_by_ap_id(ap_id) do
cached_user = get_cached_by_ap_id(ap_id)
maybe_fetched_user = needs_update?(cached_user) && fetch_by_ap_id(ap_id)
case {cached_user, maybe_fetched_user} do
{_, {:ok, %User{} = user}} ->
{:ok, user}
{%User{} = user, _} ->
{:ok, user}
_ ->
{:error, :not_found}
end
end
@doc """
Creates an internal service actor by URI if missing.
Optionally takes nickname for addressing.
"""
@spec get_or_create_service_actor_by_ap_id(String.t(), String.t()) :: User.t() | nil
def get_or_create_service_actor_by_ap_id(uri, nickname) do
{_, user} =
case get_cached_by_ap_id(uri) do
nil ->
with {:error, %{errors: errors}} <- create_service_actor(uri, nickname) do
Logger.error("Cannot create service actor: #{uri}/.\n#{inspect(errors)}")
{:error, nil}
end
%User{invisible: false} = user ->
set_invisible(user)
user ->
{:ok, user}
end
user
end
@spec set_invisible(User.t()) :: {:ok, User.t()}
defp set_invisible(user) do
user
|> change(%{invisible: true})
|> update_and_set_cache()
end
@spec create_service_actor(String.t(), String.t()) ::
{:ok, User.t()} | {:error, Ecto.Changeset.t()}
defp create_service_actor(uri, nickname) do
%User{
invisible: true,
local: true,
ap_id: uri,
nickname: nickname,
follower_address: uri <> "/followers"
}
|> change
|> unique_constraint(:nickname)
|> Repo.insert()
|> set_cache()
end
def public_key(%{public_key: public_key_pem}) when is_binary(public_key_pem) do
key =
public_key_pem
|> :public_key.pem_decode()
|> hd()
|> :public_key.pem_entry_decode()
{:ok, key}
end
def public_key(_), do: {:error, "key not found"}
def get_public_key_for_ap_id(ap_id) do
with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
{:ok, public_key} <- public_key(user) do
{:ok, public_key}
else
_ -> :error
end
end
def ap_enabled?(%User{local: true}), do: true
def ap_enabled?(%User{ap_enabled: ap_enabled}), do: ap_enabled
def ap_enabled?(_), do: false
@doc "Gets or fetch a user by uri or nickname."
@spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
# wait a period of time and return newest version of the User structs
# this is because we have synchronous follow APIs and need to simulate them
# with an async handshake
def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
with %User{} = a <- get_cached_by_id(a.id),
%User{} = b <- get_cached_by_id(b.id) do
{:ok, a, b}
else
nil -> :error
end
end
def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
with :ok <- :timer.sleep(timeout),
%User{} = a <- get_cached_by_id(a.id),
%User{} = b <- get_cached_by_id(b.id) do
{:ok, a, b}
else
nil -> :error
end
end
def parse_bio(bio) when is_binary(bio) and bio != "" do
bio
|> CommonUtils.format_input("text/plain", mentions_format: :full)
|> elem(0)
end
def parse_bio(_), do: ""
def parse_bio(bio, user) when is_binary(bio) and bio != "" do
# TODO: get profile URLs other than user.ap_id
profile_urls = [user.ap_id]
bio
|> CommonUtils.format_input("text/plain",
mentions_format: :full,
rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
)
|> elem(0)
end
def parse_bio(_, _), do: ""
def tag(user_identifiers, tags) when is_list(user_identifiers) do
Repo.transaction(fn ->
for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
end)
end
def tag(nickname, tags) when is_binary(nickname),
do: tag(get_by_nickname(nickname), tags)
def tag(%User{} = user, tags),
do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
def untag(user_identifiers, tags) when is_list(user_identifiers) do
Repo.transaction(fn ->
for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
end)
end
def untag(nickname, tags) when is_binary(nickname),
do: untag(get_by_nickname(nickname), tags)
def untag(%User{} = user, tags),
do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
defp update_tags(%User{} = user, new_tags) do
{:ok, updated_user} =
user
|> change(%{tags: new_tags})
|> update_and_set_cache()
updated_user
end
defp normalize_tags(tags) do
[tags]
|> List.flatten()
|> Enum.map(&String.downcase/1)
end
defp local_nickname_regex do
if Config.get([:instance, :extended_nickname_format]) do
@extended_local_nickname_regex
else
@strict_local_nickname_regex
end
end
def local_nickname(nickname_or_mention) do
nickname_or_mention
|> full_nickname()
|> String.split("@")
|> hd()
end
def full_nickname(nickname_or_mention),
do: String.trim_leading(nickname_or_mention, "@")
def error_user(ap_id) do
%User{
name: ap_id,
ap_id: ap_id,
nickname: "erroruser@example.com",
inserted_at: NaiveDateTime.utc_now()
}
end
@spec all_superusers() :: [User.t()]
def all_superusers do
User.Query.build(%{super_users: true, local: true, deactivated: false})
|> Repo.all()
end
def muting_reblogs?(%User{} = user, %User{} = target) do
UserRelationship.reblog_mute_exists?(user, target)
end
def showing_reblogs?(%User{} = user, %User{} = target) do
not muting_reblogs?(user, target)
end
@doc """
The function returns a query to get users with no activity for given interval of days.
Inactive users are those who didn't read any notification, or had any activity where
the user is the activity's actor, during `inactivity_threshold` days.
Deactivated users will not appear in this list.
## Examples
iex> Pleroma.User.list_inactive_users()
%Ecto.Query{}
"""
@spec list_inactive_users_query(integer()) :: Ecto.Query.t()
def list_inactive_users_query(inactivity_threshold \\ 7) do
negative_inactivity_threshold = -inactivity_threshold
now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
# Subqueries are not supported in `where` clauses, join gets too complicated.
has_read_notifications =
from(n in Pleroma.Notification,
where: n.seen == true,
group_by: n.id,
having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
select: n.user_id
)
|> Pleroma.Repo.all()
from(u in Pleroma.User,
left_join: a in Pleroma.Activity,
on: u.ap_id == a.actor,
where: not is_nil(u.nickname),
where: u.deactivated != ^true,
where: u.id not in ^has_read_notifications,
group_by: u.id,
having:
max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
is_nil(max(a.inserted_at))
)
end
@doc """
Enable or disable email notifications for user
## Examples
iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => false}}, "digest", true)
Pleroma.User{email_notifications: %{"digest" => true}}
iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => true}}, "digest", false)
Pleroma.User{email_notifications: %{"digest" => false}}
"""
@spec switch_email_notifications(t(), String.t(), boolean()) ::
{:ok, t()} | {:error, Ecto.Changeset.t()}
def switch_email_notifications(user, type, status) do
User.update_email_notifications(user, %{type => status})
end
@doc """
Set `last_digest_emailed_at` value for the user to current time
"""
@spec touch_last_digest_emailed_at(t()) :: t()
def touch_last_digest_emailed_at(user) do
now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
{:ok, updated_user} =
user
|> change(%{last_digest_emailed_at: now})
|> update_and_set_cache()
updated_user
end
@spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
def toggle_confirmation(%User{} = user) do
user
|> confirmation_changeset(need_confirmation: !user.confirmation_pending)
|> update_and_set_cache()
end
@spec toggle_confirmation([User.t()]) :: [{:ok, User.t()} | {:error, Changeset.t()}]
def toggle_confirmation(users) do
Enum.map(users, &toggle_confirmation/1)
end
def get_mascot(%{mascot: %{} = mascot}) when not is_nil(mascot) do
mascot
end
def get_mascot(%{mascot: mascot}) when is_nil(mascot) do
# use instance-default
config = Config.get([:assets, :mascots])
default_mascot = Config.get([:assets, :default_mascot])
mascot = Keyword.get(config, default_mascot)
%{
"id" => "default-mascot",
"url" => mascot[:url],
"preview_url" => mascot[:url],
"pleroma" => %{
"mime_type" => mascot[:mime_type]
}
}
end
def ensure_keys_present(%{keys: keys} = user) when not is_nil(keys), do: {:ok, user}
def ensure_keys_present(%User{} = user) do
with {:ok, pem} <- Keys.generate_rsa_pem() do
user
|> cast(%{keys: pem}, [:keys])
|> validate_required([:keys])
|> update_and_set_cache()
end
end
def get_ap_ids_by_nicknames(nicknames) do
from(u in User,
where: u.nickname in ^nicknames,
select: u.ap_id
)
|> Repo.all()
end
defp put_password_hash(
%Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
) do
change(changeset, password_hash: Pbkdf2.hash_pwd_salt(password))
end
defp put_password_hash(changeset), do: changeset
def is_internal_user?(%User{nickname: nil}), do: true
def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
def is_internal_user?(_), do: false
# A hack because user delete activities have a fake id for whatever reason
# TODO: Get rid of this
def get_delivered_users_by_object_id("pleroma:fake_object_id"), do: []
def get_delivered_users_by_object_id(object_id) do
from(u in User,
inner_join: delivery in assoc(u, :deliveries),
where: delivery.object_id == ^object_id
)
|> Repo.all()
end
def change_email(user, email) do
user
|> cast(%{email: email}, [:email])
|> validate_required([:email])
|> unique_constraint(:email)
|> validate_format(:email, @email_regex)
|> update_and_set_cache()
end
# Internal function; public one is `deactivate/2`
defp set_activation_status(user, deactivated) do
user
|> cast(%{deactivated: deactivated}, [:deactivated])
|> update_and_set_cache()
end
def update_banner(user, banner) do
user
|> cast(%{banner: banner}, [:banner])
|> update_and_set_cache()
end
def update_background(user, background) do
user
|> cast(%{background: background}, [:background])
|> update_and_set_cache()
end
def roles(%{is_moderator: is_moderator, is_admin: is_admin}) do
%{
admin: is_admin,
moderator: is_moderator
}
end
def validate_fields(changeset, remote? \\ false) do
limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields
limit = Config.get([:instance, limit_name], 0)
changeset
|> validate_length(:fields, max: limit)
|> validate_change(:fields, fn :fields, fields ->
if Enum.all?(fields, &valid_field?/1) do
[]
else
[fields: "invalid"]
end
end)
end
defp valid_field?(%{"name" => name, "value" => value}) do
name_limit = Config.get([:instance, :account_field_name_length], 255)
value_limit = Config.get([:instance, :account_field_value_length], 255)
is_binary(name) && is_binary(value) && String.length(name) <= name_limit &&
String.length(value) <= value_limit
end
defp valid_field?(_), do: false
defp truncate_field(%{"name" => name, "value" => value}) do
{name, _chopped} =
String.split_at(name, Config.get([:instance, :account_field_name_length], 255))
{value, _chopped} =
String.split_at(value, Config.get([:instance, :account_field_value_length], 255))
%{"name" => name, "value" => value}
end
def admin_api_update(user, params) do
user
|> cast(params, [
:is_moderator,
:is_admin,
:show_role
])
|> update_and_set_cache()
end
@doc "Signs user out of all applications"
def global_sign_out(user) do
OAuth.Authorization.delete_user_authorizations(user)
OAuth.Token.delete_user_tokens(user)
end
def mascot_update(user, url) do
user
|> cast(%{mascot: url}, [:mascot])
|> validate_required([:mascot])
|> update_and_set_cache()
end
def mastodon_settings_update(user, settings) do
user
|> cast(%{mastofe_settings: settings}, [:mastofe_settings])
|> validate_required([:mastofe_settings])
|> update_and_set_cache()
end
@spec confirmation_changeset(User.t(), keyword()) :: Changeset.t()
def confirmation_changeset(user, need_confirmation: need_confirmation?) do
params =
if need_confirmation? do
%{
confirmation_pending: true,
confirmation_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64()
}
else
%{
confirmation_pending: false,
confirmation_token: nil
}
end
cast(user, params, [:confirmation_pending, :confirmation_token])
end
@spec approval_changeset(User.t(), keyword()) :: Changeset.t()
def approval_changeset(user, need_approval: need_approval?) do
params = if need_approval?, do: %{approval_pending: true}, else: %{approval_pending: false}
cast(user, params, [:approval_pending])
end
def add_pinnned_activity(user, %Pleroma.Activity{id: id}) do
if id not in user.pinned_activities do
max_pinned_statuses = Config.get([:instance, :max_pinned_statuses], 0)
params = %{pinned_activities: user.pinned_activities ++ [id]}
user
|> cast(params, [:pinned_activities])
|> validate_length(:pinned_activities,
max: max_pinned_statuses,
message: "You have already pinned the maximum number of statuses"
)
else
change(user)
end
|> update_and_set_cache()
end
def remove_pinnned_activity(user, %Pleroma.Activity{id: id}) do
params = %{pinned_activities: List.delete(user.pinned_activities, id)}
user
|> cast(params, [:pinned_activities])
|> update_and_set_cache()
end
def update_email_notifications(user, settings) do
email_notifications =
user.email_notifications
|> Map.merge(settings)
|> Map.take(["digest"])
params = %{email_notifications: email_notifications}
fields = [:email_notifications]
user
|> cast(params, fields)
|> validate_required(fields)
|> update_and_set_cache()
end
defp set_domain_blocks(user, domain_blocks) do
params = %{domain_blocks: domain_blocks}
user
|> cast(params, [:domain_blocks])
|> validate_required([:domain_blocks])
|> update_and_set_cache()
end
def block_domain(user, domain_blocked) do
set_domain_blocks(user, Enum.uniq([domain_blocked | user.domain_blocks]))
end
def unblock_domain(user, domain_blocked) do
set_domain_blocks(user, List.delete(user.domain_blocks, domain_blocked))
end
@spec add_to_block(User.t(), User.t()) ::
{:ok, UserRelationship.t()} | {:error, Ecto.Changeset.t()}
defp add_to_block(%User{} = user, %User{} = blocked) do
UserRelationship.create_block(user, blocked)
end
@spec add_to_block(User.t(), User.t()) ::
{:ok, UserRelationship.t()} | {:ok, nil} | {:error, Ecto.Changeset.t()}
defp remove_from_block(%User{} = user, %User{} = blocked) do
UserRelationship.delete_block(user, blocked)
end
- defp add_to_mutes(%User{} = user, %User{} = muted_user, notifications?) do
- with {:ok, user_mute} <- UserRelationship.create_mute(user, muted_user),
- {:ok, user_notification_mute} <-
- (notifications? && UserRelationship.create_notification_mute(user, muted_user)) ||
- {:ok, nil} do
- {:ok, Enum.filter([user_mute, user_notification_mute], & &1)}
- end
- end
-
- defp remove_from_mutes(user, %User{} = muted_user) do
- with {:ok, user_mute} <- UserRelationship.delete_mute(user, muted_user),
- {:ok, user_notification_mute} <-
- UserRelationship.delete_notification_mute(user, muted_user) do
- {:ok, [user_mute, user_notification_mute]}
- end
- end
-
def set_invisible(user, invisible) do
params = %{invisible: invisible}
user
|> cast(params, [:invisible])
|> validate_required([:invisible])
|> update_and_set_cache()
end
def sanitize_html(%User{} = user) do
sanitize_html(user, nil)
end
# User data that mastodon isn't filtering (treated as plaintext):
# - field name
# - display name
def sanitize_html(%User{} = user, filter) do
fields =
Enum.map(user.fields, fn %{"name" => name, "value" => value} ->
%{
"name" => name,
"value" => HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
}
end)
user
|> Map.put(:bio, HTML.filter_tags(user.bio, filter))
|> Map.put(:fields, fields)
end
end
diff --git a/lib/pleroma/web/api_spec/operations/account_operation.ex b/lib/pleroma/web/api_spec/operations/account_operation.ex
index aaebc9b5c..de715a077 100644
--- a/lib/pleroma/web/api_spec/operations/account_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/account_operation.ex
@@ -1,766 +1,779 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.AccountOperation do
alias OpenApiSpex.Operation
alias OpenApiSpex.Reference
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Schemas.Account
alias Pleroma.Web.ApiSpec.Schemas.AccountRelationship
alias Pleroma.Web.ApiSpec.Schemas.ActorType
alias Pleroma.Web.ApiSpec.Schemas.ApiError
alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
alias Pleroma.Web.ApiSpec.Schemas.List
alias Pleroma.Web.ApiSpec.Schemas.Status
alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope
import Pleroma.Web.ApiSpec.Helpers
@spec open_api_operation(atom) :: Operation.t()
def open_api_operation(action) do
operation = String.to_existing_atom("#{action}_operation")
apply(__MODULE__, operation, [])
end
@spec create_operation() :: Operation.t()
def create_operation do
%Operation{
tags: ["accounts"],
summary: "Register an account",
description:
"Creates a user and account records. Returns an account access token for the app that initiated the request. The app should save this token for later, and should wait for the user to confirm their account by clicking a link in their email inbox.",
operationId: "AccountController.create",
requestBody: request_body("Parameters", create_request(), required: true),
responses: %{
200 => Operation.response("Account", "application/json", create_response()),
400 => Operation.response("Error", "application/json", ApiError),
403 => Operation.response("Error", "application/json", ApiError),
429 => Operation.response("Error", "application/json", ApiError)
}
}
end
def verify_credentials_operation do
%Operation{
tags: ["accounts"],
description: "Test to make sure that the user token works.",
summary: "Verify account credentials",
operationId: "AccountController.verify_credentials",
security: [%{"oAuth" => ["read:accounts"]}],
responses: %{
200 => Operation.response("Account", "application/json", Account)
}
}
end
def update_credentials_operation do
%Operation{
tags: ["accounts"],
summary: "Update account credentials",
description: "Update the user's display and preferences.",
operationId: "AccountController.update_credentials",
security: [%{"oAuth" => ["write:accounts"]}],
requestBody: request_body("Parameters", update_credentials_request(), required: true),
responses: %{
200 => Operation.response("Account", "application/json", Account),
403 => Operation.response("Error", "application/json", ApiError)
}
}
end
def relationships_operation do
%Operation{
tags: ["accounts"],
summary: "Check relationships to other accounts",
operationId: "AccountController.relationships",
description: "Find out whether a given account is followed, blocked, muted, etc.",
security: [%{"oAuth" => ["read:follows"]}],
parameters: [
Operation.parameter(
:id,
:query,
%Schema{
oneOf: [%Schema{type: :array, items: %Schema{type: :string}}, %Schema{type: :string}]
},
"Account IDs",
example: "123"
)
],
responses: %{
200 => Operation.response("Account", "application/json", array_of_relationships())
}
}
end
def show_operation do
%Operation{
tags: ["accounts"],
summary: "Account",
operationId: "AccountController.show",
description: "View information about a profile.",
parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}],
responses: %{
200 => Operation.response("Account", "application/json", Account),
401 => Operation.response("Error", "application/json", ApiError),
404 => Operation.response("Error", "application/json", ApiError)
}
}
end
def statuses_operation do
%Operation{
tags: ["accounts"],
summary: "Statuses",
operationId: "AccountController.statuses",
description:
"Statuses posted to the given account. Public (for public statuses only), or user token + `read:statuses` (for private statuses the user is authorized to see)",
parameters:
[
%Reference{"$ref": "#/components/parameters/accountIdOrNickname"},
Operation.parameter(:pinned, :query, BooleanLike, "Include only pinned statuses"),
Operation.parameter(:tagged, :query, :string, "With tag"),
Operation.parameter(
:only_media,
:query,
BooleanLike,
"Include only statuses with media attached"
),
Operation.parameter(
:with_muted,
:query,
BooleanLike,
"Include statuses from muted acccounts."
),
Operation.parameter(:exclude_reblogs, :query, BooleanLike, "Exclude reblogs"),
Operation.parameter(:exclude_replies, :query, BooleanLike, "Exclude replies"),
Operation.parameter(
:exclude_visibilities,
:query,
%Schema{type: :array, items: VisibilityScope},
"Exclude visibilities"
)
] ++ pagination_params(),
responses: %{
200 => Operation.response("Statuses", "application/json", array_of_statuses()),
401 => Operation.response("Error", "application/json", ApiError),
404 => Operation.response("Error", "application/json", ApiError)
}
}
end
def followers_operation do
%Operation{
tags: ["accounts"],
summary: "Followers",
operationId: "AccountController.followers",
security: [%{"oAuth" => ["read:accounts"]}],
description:
"Accounts which follow the given account, if network is not hidden by the account owner.",
parameters: [
%Reference{"$ref": "#/components/parameters/accountIdOrNickname"},
Operation.parameter(:id, :query, :string, "ID of the resource owner"),
with_relationships_param() | pagination_params()
],
responses: %{
200 => Operation.response("Accounts", "application/json", array_of_accounts())
}
}
end
def following_operation do
%Operation{
tags: ["accounts"],
summary: "Following",
operationId: "AccountController.following",
security: [%{"oAuth" => ["read:accounts"]}],
description:
"Accounts which the given account is following, if network is not hidden by the account owner.",
parameters: [
%Reference{"$ref": "#/components/parameters/accountIdOrNickname"},
Operation.parameter(:id, :query, :string, "ID of the resource owner"),
with_relationships_param() | pagination_params()
],
responses: %{200 => Operation.response("Accounts", "application/json", array_of_accounts())}
}
end
def lists_operation do
%Operation{
tags: ["accounts"],
summary: "Lists containing this account",
operationId: "AccountController.lists",
security: [%{"oAuth" => ["read:lists"]}],
description: "User lists that you have added this account to.",
parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}],
responses: %{200 => Operation.response("Lists", "application/json", array_of_lists())}
}
end
def follow_operation do
%Operation{
tags: ["accounts"],
summary: "Follow",
operationId: "AccountController.follow",
security: [%{"oAuth" => ["follow", "write:follows"]}],
description: "Follow the given account",
parameters: [
%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}
],
requestBody:
request_body(
"Parameters",
%Schema{
type: :object,
properties: %{
reblogs: %Schema{
type: :boolean,
description: "Receive this account's reblogs in home timeline? Defaults to true.",
default: true
}
}
},
required: false
),
responses: %{
200 => Operation.response("Relationship", "application/json", AccountRelationship),
400 => Operation.response("Error", "application/json", ApiError),
404 => Operation.response("Error", "application/json", ApiError)
}
}
end
def unfollow_operation do
%Operation{
tags: ["accounts"],
summary: "Unfollow",
operationId: "AccountController.unfollow",
security: [%{"oAuth" => ["follow", "write:follows"]}],
description: "Unfollow the given account",
parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}],
responses: %{
200 => Operation.response("Relationship", "application/json", AccountRelationship),
400 => Operation.response("Error", "application/json", ApiError),
404 => Operation.response("Error", "application/json", ApiError)
}
}
end
def mute_operation do
%Operation{
tags: ["accounts"],
summary: "Mute",
operationId: "AccountController.mute",
security: [%{"oAuth" => ["follow", "write:mutes"]}],
requestBody: request_body("Parameters", mute_request()),
description:
"Mute the given account. Clients should filter statuses and notifications from this account, if received (e.g. due to a boost in the Home timeline).",
parameters: [
%Reference{"$ref": "#/components/parameters/accountIdOrNickname"},
Operation.parameter(
:notifications,
:query,
%Schema{allOf: [BooleanLike], default: true},
"Mute notifications in addition to statuses? Defaults to `true`."
+ ),
+ Operation.parameter(
+ :expires_in,
+ :query,
+ %Schema{type: :integer, default: 0},
+ "Expire the mute in `expires_in` seconds. Default 0 for infinity"
)
],
responses: %{
200 => Operation.response("Relationship", "application/json", AccountRelationship)
}
}
end
def unmute_operation do
%Operation{
tags: ["accounts"],
summary: "Unmute",
operationId: "AccountController.unmute",
security: [%{"oAuth" => ["follow", "write:mutes"]}],
description: "Unmute the given account.",
parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}],
responses: %{
200 => Operation.response("Relationship", "application/json", AccountRelationship)
}
}
end
def block_operation do
%Operation{
tags: ["accounts"],
summary: "Block",
operationId: "AccountController.block",
security: [%{"oAuth" => ["follow", "write:blocks"]}],
description:
"Block the given account. Clients should filter statuses from this account if received (e.g. due to a boost in the Home timeline)",
parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}],
responses: %{
200 => Operation.response("Relationship", "application/json", AccountRelationship)
}
}
end
def unblock_operation do
%Operation{
tags: ["accounts"],
summary: "Unblock",
operationId: "AccountController.unblock",
security: [%{"oAuth" => ["follow", "write:blocks"]}],
description: "Unblock the given account.",
parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}],
responses: %{
200 => Operation.response("Relationship", "application/json", AccountRelationship)
}
}
end
def follow_by_uri_operation do
%Operation{
tags: ["accounts"],
summary: "Follow by URI",
operationId: "AccountController.follows",
security: [%{"oAuth" => ["follow", "write:follows"]}],
requestBody: request_body("Parameters", follow_by_uri_request(), required: true),
responses: %{
200 => Operation.response("Account", "application/json", AccountRelationship),
400 => Operation.response("Error", "application/json", ApiError),
404 => Operation.response("Error", "application/json", ApiError)
}
}
end
def mutes_operation do
%Operation{
tags: ["accounts"],
summary: "Muted accounts",
operationId: "AccountController.mutes",
description: "Accounts the user has muted.",
security: [%{"oAuth" => ["follow", "read:mutes"]}],
responses: %{
200 => Operation.response("Accounts", "application/json", array_of_accounts())
}
}
end
def blocks_operation do
%Operation{
tags: ["accounts"],
summary: "Blocked users",
operationId: "AccountController.blocks",
description: "View your blocks. See also accounts/:id/{block,unblock}",
security: [%{"oAuth" => ["read:blocks"]}],
responses: %{
200 => Operation.response("Accounts", "application/json", array_of_accounts())
}
}
end
def endorsements_operation do
%Operation{
tags: ["accounts"],
summary: "Endorsements",
operationId: "AccountController.endorsements",
description: "Not implemented",
security: [%{"oAuth" => ["read:accounts"]}],
responses: %{
200 => empty_array_response()
}
}
end
def identity_proofs_operation do
%Operation{
tags: ["accounts"],
summary: "Identity proofs",
operationId: "AccountController.identity_proofs",
description: "Not implemented",
responses: %{
200 => empty_array_response()
}
}
end
defp create_request do
%Schema{
title: "AccountCreateRequest",
description: "POST body for creating an account",
type: :object,
required: [:username, :password, :agreement],
properties: %{
reason: %Schema{
type: :string,
nullable: true,
description:
"Text that will be reviewed by moderators if registrations require manual approval"
},
username: %Schema{type: :string, description: "The desired username for the account"},
email: %Schema{
type: :string,
nullable: true,
description:
"The email address to be used for login. Required when `account_activation_required` is enabled.",
format: :email
},
password: %Schema{
type: :string,
description: "The password to be used for login",
format: :password
},
agreement: %Schema{
allOf: [BooleanLike],
description:
"Whether the user agrees to the local rules, terms, and policies. These should be presented to the user in order to allow them to consent before setting this parameter to TRUE."
},
locale: %Schema{
type: :string,
nullable: true,
description: "The language of the confirmation email that will be sent"
},
# Pleroma-specific properties:
fullname: %Schema{type: :string, nullable: true, description: "Full name"},
bio: %Schema{type: :string, description: "Bio", nullable: true, default: ""},
captcha_solution: %Schema{
type: :string,
nullable: true,
description: "Provider-specific captcha solution"
},
captcha_token: %Schema{
type: :string,
nullable: true,
description: "Provider-specific captcha token"
},
captcha_answer_data: %Schema{
type: :string,
nullable: true,
description: "Provider-specific captcha data"
},
token: %Schema{
type: :string,
nullable: true,
description: "Invite token required when the registrations aren't public"
}
},
example: %{
"username" => "cofe",
"email" => "cofe@example.com",
"password" => "secret",
"agreement" => "true",
"bio" => "☕️"
}
}
end
# Note: this is a token response (if login succeeds!), but there's no oauth operation file yet.
defp create_response do
%Schema{
title: "AccountCreateResponse",
description: "Response schema for an account",
type: :object,
properties: %{
# The response when auto-login on create succeeds (token is issued):
token_type: %Schema{type: :string},
access_token: %Schema{type: :string},
refresh_token: %Schema{type: :string},
scope: %Schema{type: :string},
created_at: %Schema{type: :integer, format: :"date-time"},
me: %Schema{type: :string},
expires_in: %Schema{type: :integer},
#
# The response when registration succeeds but auto-login fails (no token):
identifier: %Schema{type: :string},
message: %Schema{type: :string}
},
required: [],
# Note: example of successful registration with failed login response:
# example: %{
# "identifier" => "missing_confirmed_email",
# "message" => "You have been registered. Please check your email for further instructions."
# },
example: %{
"token_type" => "Bearer",
"access_token" => "i9hAVVzGld86Pl5JtLtizKoXVvtTlSCJvwaugCxvZzk",
"refresh_token" => "i9hAVVzGld86Pl5JtLtizKoXVvtTlSCJvwaugCxvZzz",
"created_at" => 1_585_918_714,
"expires_in" => 600,
"scope" => "read write follow push",
"me" => "https://gensokyo.2hu/users/raymoo"
}
}
end
defp update_credentials_request do
%Schema{
title: "AccountUpdateCredentialsRequest",
description: "POST body for creating an account",
type: :object,
properties: %{
bot: %Schema{
allOf: [BooleanLike],
nullable: true,
description: "Whether the account has a bot flag."
},
display_name: %Schema{
type: :string,
nullable: true,
description: "The display name to use for the profile."
},
note: %Schema{type: :string, description: "The account bio."},
avatar: %Schema{
type: :string,
nullable: true,
description: "Avatar image encoded using multipart/form-data",
format: :binary
},
header: %Schema{
type: :string,
nullable: true,
description: "Header image encoded using multipart/form-data",
format: :binary
},
locked: %Schema{
allOf: [BooleanLike],
nullable: true,
description: "Whether manual approval of follow requests is required."
},
accepts_chat_messages: %Schema{
allOf: [BooleanLike],
nullable: true,
description: "Whether the user accepts receiving chat messages."
},
fields_attributes: %Schema{
nullable: true,
oneOf: [
%Schema{type: :array, items: attribute_field()},
%Schema{type: :object, additionalProperties: %Schema{type: attribute_field()}}
]
},
# NOTE: `source` field is not supported
#
# source: %Schema{
# type: :object,
# properties: %{
# privacy: %Schema{type: :string},
# sensitive: %Schema{type: :boolean},
# language: %Schema{type: :string}
# }
# },
# Pleroma-specific fields
no_rich_text: %Schema{
allOf: [BooleanLike],
nullable: true,
description: "html tags are stripped from all statuses requested from the API"
},
hide_followers: %Schema{
allOf: [BooleanLike],
nullable: true,
description: "user's followers will be hidden"
},
hide_follows: %Schema{
allOf: [BooleanLike],
nullable: true,
description: "user's follows will be hidden"
},
hide_followers_count: %Schema{
allOf: [BooleanLike],
nullable: true,
description: "user's follower count will be hidden"
},
hide_follows_count: %Schema{
allOf: [BooleanLike],
nullable: true,
description: "user's follow count will be hidden"
},
hide_favorites: %Schema{
allOf: [BooleanLike],
nullable: true,
description: "user's favorites timeline will be hidden"
},
show_role: %Schema{
allOf: [BooleanLike],
nullable: true,
description: "user's role (e.g admin, moderator) will be exposed to anyone in the
API"
},
default_scope: VisibilityScope,
pleroma_settings_store: %Schema{
type: :object,
nullable: true,
description: "Opaque user settings to be saved on the backend."
},
skip_thread_containment: %Schema{
allOf: [BooleanLike],
nullable: true,
description: "Skip filtering out broken threads"
},
allow_following_move: %Schema{
allOf: [BooleanLike],
nullable: true,
description: "Allows automatically follow moved following accounts"
},
pleroma_background_image: %Schema{
type: :string,
nullable: true,
description: "Sets the background image of the user.",
format: :binary
},
discoverable: %Schema{
allOf: [BooleanLike],
nullable: true,
description:
"Discovery of this account in search results and other services is allowed."
},
actor_type: ActorType
},
example: %{
bot: false,
display_name: "cofe",
note: "foobar",
fields_attributes: [%{name: "foo", value: "bar"}],
no_rich_text: false,
hide_followers: true,
hide_follows: false,
hide_followers_count: false,
hide_follows_count: false,
hide_favorites: false,
show_role: false,
default_scope: "private",
pleroma_settings_store: %{"pleroma-fe" => %{"key" => "val"}},
skip_thread_containment: false,
allow_following_move: false,
discoverable: false,
actor_type: "Person"
}
}
end
def array_of_accounts do
%Schema{
title: "ArrayOfAccounts",
type: :array,
items: Account,
example: [Account.schema().example]
}
end
defp array_of_relationships do
%Schema{
title: "ArrayOfRelationships",
description: "Response schema for account relationships",
type: :array,
items: AccountRelationship,
example: [
%{
"id" => "1",
"following" => true,
"showing_reblogs" => true,
"followed_by" => true,
"blocking" => false,
"blocked_by" => true,
"muting" => false,
"muting_notifications" => false,
"requested" => false,
"domain_blocking" => false,
"subscribing" => false,
"endorsed" => true
},
%{
"id" => "2",
"following" => true,
"showing_reblogs" => true,
"followed_by" => true,
"blocking" => false,
"blocked_by" => true,
"muting" => true,
"muting_notifications" => false,
"requested" => true,
"domain_blocking" => false,
"subscribing" => false,
"endorsed" => false
},
%{
"id" => "3",
"following" => true,
"showing_reblogs" => true,
"followed_by" => true,
"blocking" => true,
"blocked_by" => false,
"muting" => true,
"muting_notifications" => false,
"requested" => false,
"domain_blocking" => true,
"subscribing" => true,
"endorsed" => false
}
]
}
end
defp follow_by_uri_request do
%Schema{
title: "AccountFollowsRequest",
description: "POST body for muting an account",
type: :object,
properties: %{
uri: %Schema{type: :string, nullable: true, format: :uri}
},
required: [:uri]
}
end
defp mute_request do
%Schema{
title: "AccountMuteRequest",
description: "POST body for muting an account",
type: :object,
properties: %{
notifications: %Schema{
allOf: [BooleanLike],
nullable: true,
description: "Mute notifications in addition to statuses? Defaults to true.",
default: true
+ },
+ expires_in: %Schema{
+ type: :integer,
+ nullable: true,
+ description: "Expire the mute in `expires_in` seconds. Default 0 for infinity",
+ default: 0
}
},
example: %{
- "notifications" => true
+ "notifications" => true,
+ "expires_in" => 86_400
}
}
end
defp array_of_lists do
%Schema{
title: "ArrayOfLists",
description: "Response schema for lists",
type: :array,
items: List,
example: [
%{"id" => "123", "title" => "my list"},
%{"id" => "1337", "title" => "anotehr list"}
]
}
end
defp array_of_statuses do
%Schema{
title: "ArrayOfStatuses",
type: :array,
items: Status
}
end
defp attribute_field do
%Schema{
title: "AccountAttributeField",
description: "Request schema for account custom fields",
type: :object,
properties: %{
name: %Schema{type: :string},
value: %Schema{type: :string}
},
required: [:name, :value],
example: %{
"name" => "Website",
"value" => "https://pleroma.com"
}
}
end
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
index 95d8452df..ca1a79f5e 100644
--- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
@@ -1,461 +1,461 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.AccountController do
use Pleroma.Web, :controller
import Pleroma.Web.ControllerHelper,
only: [
add_link_headers: 2,
truthy_param?: 1,
assign_account_by_id: 2,
embed_relationships?: 1,
json_response: 3
]
alias Pleroma.Maps
alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.Plugs.RateLimiter
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.ListView
alias Pleroma.Web.MastodonAPI.MastodonAPI
alias Pleroma.Web.MastodonAPI.MastodonAPIController
alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.OAuth.OAuthController
alias Pleroma.Web.OAuth.OAuthView
alias Pleroma.Web.TwitterAPI.TwitterAPI
plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(:skip_plug, [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :create)
plug(:skip_plug, EnsurePublicOrAuthenticatedPlug when action in [:show, :statuses])
plug(
OAuthScopesPlug,
%{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]}
when action in [:show, :followers, :following]
)
plug(
OAuthScopesPlug,
%{fallback: :proceed_unauthenticated, scopes: ["read:statuses"]}
when action == :statuses
)
plug(
OAuthScopesPlug,
%{scopes: ["read:accounts"]}
when action in [:verify_credentials, :endorsements, :identity_proofs]
)
plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action == :update_credentials)
plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :lists)
plug(
OAuthScopesPlug,
%{scopes: ["follow", "read:blocks"]} when action == :blocks
)
plug(
OAuthScopesPlug,
%{scopes: ["follow", "write:blocks"]} when action in [:block, :unblock]
)
plug(OAuthScopesPlug, %{scopes: ["read:follows"]} when action == :relationships)
plug(
OAuthScopesPlug,
%{scopes: ["follow", "write:follows"]} when action in [:follow_by_uri, :follow, :unfollow]
)
plug(OAuthScopesPlug, %{scopes: ["follow", "read:mutes"]} when action == :mutes)
plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute])
@relationship_actions [:follow, :unfollow]
@needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a
plug(
RateLimiter,
[name: :relation_id_action, params: [:id, :uri]] when action in @relationship_actions
)
plug(RateLimiter, [name: :relations_actions] when action in @relationship_actions)
plug(RateLimiter, [name: :app_account_creation] when action == :create)
plug(:assign_account_by_id when action in @needs_account)
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.AccountOperation
@doc "POST /api/v1/accounts"
def create(%{assigns: %{app: app}, body_params: params} = conn, _params) do
with :ok <- validate_email_param(params),
:ok <- TwitterAPI.validate_captcha(app, params),
{:ok, user} <- TwitterAPI.register_user(params),
{_, {:ok, token}} <-
{:login, OAuthController.login(user, app, app.scopes)} do
json(conn, OAuthView.render("token.json", %{user: user, token: token}))
else
{:login, {:account_status, :confirmation_pending}} ->
json_response(conn, :ok, %{
message: "You have been registered. Please check your email for further instructions.",
identifier: "missing_confirmed_email"
})
{:login, {:account_status, :approval_pending}} ->
json_response(conn, :ok, %{
message:
"You have been registered. You'll be able to log in once your account is approved.",
identifier: "awaiting_approval"
})
{:login, _} ->
json_response(conn, :ok, %{
message:
"You have been registered. Some post-registration steps may be pending. " <>
"Please log in manually.",
identifier: "manual_login_required"
})
{:error, error} ->
json_response(conn, :bad_request, %{error: error})
end
end
def create(%{assigns: %{app: _app}} = conn, _) do
render_error(conn, :bad_request, "Missing parameters")
end
def create(conn, _) do
render_error(conn, :forbidden, "Invalid credentials")
end
defp validate_email_param(%{email: email}) when not is_nil(email), do: :ok
defp validate_email_param(_) do
case Pleroma.Config.get([:instance, :account_activation_required]) do
true -> {:error, dgettext("errors", "Missing parameter: %{name}", name: "email")}
_ -> :ok
end
end
@doc "GET /api/v1/accounts/verify_credentials"
def verify_credentials(%{assigns: %{user: user}} = conn, _) do
chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
render(conn, "show.json",
user: user,
for: user,
with_pleroma_settings: true,
with_chat_token: chat_token
)
end
@doc "PATCH /api/v1/accounts/update_credentials"
def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _params) do
params =
params
|> Enum.filter(fn {_, value} -> not is_nil(value) end)
|> Enum.into(%{})
# We use an empty string as a special value to reset
# avatars, banners, backgrounds
user_image_value = fn
"" -> {:ok, nil}
value -> {:ok, value}
end
user_params =
[
:no_rich_text,
:locked,
:hide_followers_count,
:hide_follows_count,
:hide_followers,
:hide_follows,
:hide_favorites,
:show_role,
:skip_thread_containment,
:allow_following_move,
:discoverable,
:accepts_chat_messages
]
|> Enum.reduce(%{}, fn key, acc ->
Maps.put_if_present(acc, key, params[key], &{:ok, truthy_param?(&1)})
end)
|> Maps.put_if_present(:name, params[:display_name])
|> Maps.put_if_present(:bio, params[:note])
|> Maps.put_if_present(:raw_bio, params[:note])
|> Maps.put_if_present(:avatar, params[:avatar], user_image_value)
|> Maps.put_if_present(:banner, params[:header], user_image_value)
|> Maps.put_if_present(:background, params[:pleroma_background_image], user_image_value)
|> Maps.put_if_present(
:raw_fields,
params[:fields_attributes],
&{:ok, normalize_fields_attributes(&1)}
)
|> Maps.put_if_present(:pleroma_settings_store, params[:pleroma_settings_store])
|> Maps.put_if_present(:default_scope, params[:default_scope])
|> Maps.put_if_present(:default_scope, params["source"]["privacy"])
|> Maps.put_if_present(:actor_type, params[:bot], fn bot ->
if bot, do: {:ok, "Service"}, else: {:ok, "Person"}
end)
|> Maps.put_if_present(:actor_type, params[:actor_type])
# What happens here:
#
# We want to update the user through the pipeline, but the ActivityPub
# update information is not quite enough for this, because this also
# contains local settings that don't federate and don't even appear
# in the Update activity.
#
# So we first build the normal local changeset, then apply it to the
# user data, but don't persist it. With this, we generate the object
# data for our update activity. We feed this and the changeset as meta
# inforation into the pipeline, where they will be properly updated and
# federated.
with changeset <- User.update_changeset(user, user_params),
{:ok, unpersisted_user} <- Ecto.Changeset.apply_action(changeset, :update),
updated_object <-
Pleroma.Web.ActivityPub.UserView.render("user.json", user: unpersisted_user)
|> Map.delete("@context"),
{:ok, update_data, []} <- Builder.update(user, updated_object),
{:ok, _update, _} <-
Pipeline.common_pipeline(update_data,
local: true,
user_update_changeset: changeset
) do
render(conn, "show.json",
user: unpersisted_user,
for: unpersisted_user,
with_pleroma_settings: true
)
else
_e -> render_error(conn, :forbidden, "Invalid request")
end
end
defp normalize_fields_attributes(fields) do
if Enum.all?(fields, &is_tuple/1) do
Enum.map(fields, fn {_, v} -> v end)
else
Enum.map(fields, fn
%{} = field -> %{"name" => field.name, "value" => field.value}
field -> field
end)
end
end
@doc "GET /api/v1/accounts/relationships"
def relationships(%{assigns: %{user: user}} = conn, %{id: id}) do
targets = User.get_all_by_ids(List.wrap(id))
render(conn, "relationships.json", user: user, targets: targets)
end
# Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
@doc "GET /api/v1/accounts/:id"
def show(%{assigns: %{user: for_user}} = conn, %{id: nickname_or_id}) do
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
:visible <- User.visible_for(user, for_user) do
render(conn, "show.json", user: user, for: for_user)
else
error -> user_visibility_error(conn, error)
end
end
@doc "GET /api/v1/accounts/:id/statuses"
def statuses(%{assigns: %{user: reading_user}} = conn, params) do
with %User{} = user <- User.get_cached_by_nickname_or_id(params.id, for: reading_user),
:visible <- User.visible_for(user, reading_user) do
params =
params
|> Map.delete(:tagged)
|> Map.put(:tag, params[:tagged])
activities = ActivityPub.fetch_user_activities(user, reading_user, params)
conn
|> add_link_headers(activities)
|> put_view(StatusView)
|> render("index.json",
activities: activities,
for: reading_user,
as: :activity
)
else
error -> user_visibility_error(conn, error)
end
end
defp user_visibility_error(conn, error) do
case error do
:restrict_unauthenticated ->
render_error(conn, :unauthorized, "This API requires an authenticated user")
_ ->
render_error(conn, :not_found, "Can't find user")
end
end
@doc "GET /api/v1/accounts/:id/followers"
def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
params =
params
|> Enum.map(fn {key, value} -> {to_string(key), value} end)
|> Enum.into(%{})
followers =
cond do
for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
user.hide_followers -> []
true -> MastodonAPI.get_followers(user, params)
end
conn
|> add_link_headers(followers)
# https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
|> render("index.json",
for: for_user,
users: followers,
as: :user,
embed_relationships: embed_relationships?(params)
)
end
@doc "GET /api/v1/accounts/:id/following"
def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
params =
params
|> Enum.map(fn {key, value} -> {to_string(key), value} end)
|> Enum.into(%{})
followers =
cond do
for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
user.hide_follows -> []
true -> MastodonAPI.get_friends(user, params)
end
conn
|> add_link_headers(followers)
# https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
|> render("index.json",
for: for_user,
users: followers,
as: :user,
embed_relationships: embed_relationships?(params)
)
end
@doc "GET /api/v1/accounts/:id/lists"
def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
lists = Pleroma.List.get_lists_account_belongs(user, account)
conn
|> put_view(ListView)
|> render("index.json", lists: lists)
end
@doc "POST /api/v1/accounts/:id/follow"
def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
{:error, "Can not follow yourself"}
end
def follow(%{body_params: params, assigns: %{user: follower, account: followed}} = conn, _) do
with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
render(conn, "relationship.json", user: follower, target: followed)
else
{:error, message} -> json_response(conn, :forbidden, %{error: message})
end
end
@doc "POST /api/v1/accounts/:id/unfollow"
def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
{:error, "Can not unfollow yourself"}
end
def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
render(conn, "relationship.json", user: follower, target: followed)
end
end
@doc "POST /api/v1/accounts/:id/mute"
def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
- with {:ok, _user_relationships} <- User.mute(muter, muted, params.notifications) do
+ with {:ok, _user_relationships} <- User.mute(muter, muted, params) do
render(conn, "relationship.json", user: muter, target: muted)
else
{:error, message} -> json_response(conn, :forbidden, %{error: message})
end
end
@doc "POST /api/v1/accounts/:id/unmute"
def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
with {:ok, _user_relationships} <- User.unmute(muter, muted) do
render(conn, "relationship.json", user: muter, target: muted)
else
{:error, message} -> json_response(conn, :forbidden, %{error: message})
end
end
@doc "POST /api/v1/accounts/:id/block"
def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
with {:ok, _activity} <- CommonAPI.block(blocker, blocked) do
render(conn, "relationship.json", user: blocker, target: blocked)
else
{:error, message} -> json_response(conn, :forbidden, %{error: message})
end
end
@doc "POST /api/v1/accounts/:id/unblock"
def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
with {:ok, _activity} <- CommonAPI.unblock(blocker, blocked) do
render(conn, "relationship.json", user: blocker, target: blocked)
else
{:error, message} -> json_response(conn, :forbidden, %{error: message})
end
end
@doc "POST /api/v1/follows"
def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do
case User.get_cached_by_nickname(uri) do
%User{} = user ->
conn
|> assign(:account, user)
|> follow(%{})
nil ->
{:error, :not_found}
end
end
@doc "GET /api/v1/mutes"
def mutes(%{assigns: %{user: user}} = conn, _) do
users = User.muted_users(user, _restrict_deactivated = true)
render(conn, "index.json", users: users, for: user, as: :user)
end
@doc "GET /api/v1/blocks"
def blocks(%{assigns: %{user: user}} = conn, _) do
users = User.blocked_users(user, _restrict_deactivated = true)
render(conn, "index.json", users: users, for: user, as: :user)
end
@doc "GET /api/v1/endorsements"
def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params)
@doc "GET /api/v1/identity_proofs"
def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)
end
diff --git a/lib/pleroma/workers/mute_expire_worker.ex b/lib/pleroma/workers/mute_expire_worker.ex
new file mode 100644
index 000000000..b8ec939a9
--- /dev/null
+++ b/lib/pleroma/workers/mute_expire_worker.ex
@@ -0,0 +1,22 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Workers.MuteExpireWorker do
+ use Pleroma.Workers.WorkerHelper, queue: "mute_expire"
+
+ require Logger
+
+ @impl Oban.Worker
+ def perform(%Job{args: %{"op" => "unmute", "muter" => muter_id, "mutee" => mutee_id}}) do
+ muter = Pleroma.User.get_by_id(muter_id)
+ mutee = Pleroma.User.get_by_id(mutee_id)
+ Pleroma.User.unmute(muter, mutee)
+ :ok
+ end
+
+ def perform(any) do
+ Logger.error("Got call to perform(#{inspect(any)})")
+ :ok
+ end
+end
diff --git a/test/notification_test.exs b/test/notification_test.exs
index a09b08675..ffd737969 100644
--- a/test/notification_test.exs
+++ b/test/notification_test.exs
@@ -1,1142 +1,1142 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.NotificationTest do
use Pleroma.DataCase
import Pleroma.Factory
import Mock
alias Pleroma.FollowingRelationship
alias Pleroma.Notification
alias Pleroma.Repo
alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.NotificationView
alias Pleroma.Web.Push
alias Pleroma.Web.Streamer
describe "create_notifications" do
test "never returns nil" do
user = insert(:user)
other_user = insert(:user, %{invisible: true})
{:ok, activity} = CommonAPI.post(user, %{status: "yeah"})
{:ok, activity} = CommonAPI.react_with_emoji(activity.id, other_user, "☕")
refute {:ok, [nil]} == Notification.create_notifications(activity)
end
test "creates a notification for an emoji reaction" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "yeah"})
{:ok, activity} = CommonAPI.react_with_emoji(activity.id, other_user, "☕")
{:ok, [notification]} = Notification.create_notifications(activity)
assert notification.user_id == user.id
assert notification.type == "pleroma:emoji_reaction"
end
test "notifies someone when they are directly addressed" do
user = insert(:user)
other_user = insert(:user)
third_user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
status: "hey @#{other_user.nickname} and @#{third_user.nickname}"
})
{:ok, [notification, other_notification]} = Notification.create_notifications(activity)
notified_ids = Enum.sort([notification.user_id, other_notification.user_id])
assert notified_ids == [other_user.id, third_user.id]
assert notification.activity_id == activity.id
assert notification.type == "mention"
assert other_notification.activity_id == activity.id
assert [%Pleroma.Marker{unread_count: 2}] =
Pleroma.Marker.get_markers(other_user, ["notifications"])
end
test "it creates a notification for subscribed users" do
user = insert(:user)
subscriber = insert(:user)
User.subscribe(subscriber, user)
{:ok, status} = CommonAPI.post(user, %{status: "Akariiiin"})
{:ok, [notification]} = Notification.create_notifications(status)
assert notification.user_id == subscriber.id
end
test "does not create a notification for subscribed users if status is a reply" do
user = insert(:user)
other_user = insert(:user)
subscriber = insert(:user)
User.subscribe(subscriber, other_user)
{:ok, activity} = CommonAPI.post(user, %{status: "test post"})
{:ok, _reply_activity} =
CommonAPI.post(other_user, %{
status: "test reply",
in_reply_to_status_id: activity.id
})
user_notifications = Notification.for_user(user)
assert length(user_notifications) == 1
subscriber_notifications = Notification.for_user(subscriber)
assert Enum.empty?(subscriber_notifications)
end
end
describe "CommonApi.post/2 notification-related functionality" do
test_with_mock "creates but does NOT send notification to blocker user",
Push,
[:passthrough],
[] do
user = insert(:user)
blocker = insert(:user)
{:ok, _user_relationship} = User.block(blocker, user)
{:ok, _activity} = CommonAPI.post(user, %{status: "hey @#{blocker.nickname}!"})
blocker_id = blocker.id
assert [%Notification{user_id: ^blocker_id}] = Repo.all(Notification)
refute called(Push.send(:_))
end
test_with_mock "creates but does NOT send notification to notification-muter user",
Push,
[:passthrough],
[] do
user = insert(:user)
muter = insert(:user)
{:ok, _user_relationships} = User.mute(muter, user)
{:ok, _activity} = CommonAPI.post(user, %{status: "hey @#{muter.nickname}!"})
muter_id = muter.id
assert [%Notification{user_id: ^muter_id}] = Repo.all(Notification)
refute called(Push.send(:_))
end
test_with_mock "creates but does NOT send notification to thread-muter user",
Push,
[:passthrough],
[] do
user = insert(:user)
thread_muter = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "hey @#{thread_muter.nickname}!"})
{:ok, _} = CommonAPI.add_mute(thread_muter, activity)
{:ok, _same_context_activity} =
CommonAPI.post(user, %{
status: "hey-hey-hey @#{thread_muter.nickname}!",
in_reply_to_status_id: activity.id
})
[pre_mute_notification, post_mute_notification] =
Repo.all(from(n in Notification, where: n.user_id == ^thread_muter.id, order_by: n.id))
pre_mute_notification_id = pre_mute_notification.id
post_mute_notification_id = post_mute_notification.id
assert called(
Push.send(
:meck.is(fn
%Notification{id: ^pre_mute_notification_id} -> true
_ -> false
end)
)
)
refute called(
Push.send(
:meck.is(fn
%Notification{id: ^post_mute_notification_id} -> true
_ -> false
end)
)
)
end
end
describe "create_notification" do
@tag needs_streamer: true
test "it creates a notification for user and send to the 'user' and the 'user:notification' stream" do
user = insert(:user)
task =
Task.async(fn ->
Streamer.get_topic_and_add_socket("user", user)
assert_receive {:render_with_user, _, _, _}, 4_000
end)
task_user_notification =
Task.async(fn ->
Streamer.get_topic_and_add_socket("user:notification", user)
assert_receive {:render_with_user, _, _, _}, 4_000
end)
activity = insert(:note_activity)
notify = Notification.create_notification(activity, user)
assert notify.user_id == user.id
Task.await(task)
Task.await(task_user_notification)
end
test "it creates a notification for user if the user blocks the activity author" do
activity = insert(:note_activity)
author = User.get_cached_by_ap_id(activity.data["actor"])
user = insert(:user)
{:ok, _user_relationship} = User.block(user, author)
assert Notification.create_notification(activity, user)
end
test "it creates a notification for the user if the user mutes the activity author" do
muter = insert(:user)
muted = insert(:user)
{:ok, _} = User.mute(muter, muted)
muter = Repo.get(User, muter.id)
{:ok, activity} = CommonAPI.post(muted, %{status: "Hi @#{muter.nickname}"})
notification = Notification.create_notification(activity, muter)
assert notification.id
assert notification.seen
end
test "notification created if user is muted without notifications" do
muter = insert(:user)
muted = insert(:user)
- {:ok, _user_relationships} = User.mute(muter, muted, false)
+ {:ok, _user_relationships} = User.mute(muter, muted, %{notifications: false})
{:ok, activity} = CommonAPI.post(muted, %{status: "Hi @#{muter.nickname}"})
assert Notification.create_notification(activity, muter)
end
test "it creates a notification for an activity from a muted thread" do
muter = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(muter, %{status: "hey"})
CommonAPI.add_mute(muter, activity)
{:ok, activity} =
CommonAPI.post(other_user, %{
status: "Hi @#{muter.nickname}",
in_reply_to_status_id: activity.id
})
notification = Notification.create_notification(activity, muter)
assert notification.id
assert notification.seen
end
test "it disables notifications from strangers" do
follower = insert(:user)
followed =
insert(:user,
notification_settings: %Pleroma.User.NotificationSetting{block_from_strangers: true}
)
{:ok, activity} = CommonAPI.post(follower, %{status: "hey @#{followed.nickname}"})
refute Notification.create_notification(activity, followed)
end
test "it doesn't create a notification for user if he is the activity author" do
activity = insert(:note_activity)
author = User.get_cached_by_ap_id(activity.data["actor"])
refute Notification.create_notification(activity, author)
end
test "it doesn't create duplicate notifications for follow+subscribed users" do
user = insert(:user)
subscriber = insert(:user)
{:ok, _, _, _} = CommonAPI.follow(subscriber, user)
User.subscribe(subscriber, user)
{:ok, status} = CommonAPI.post(user, %{status: "Akariiiin"})
{:ok, [_notif]} = Notification.create_notifications(status)
end
test "it doesn't create subscription notifications if the recipient cannot see the status" do
user = insert(:user)
subscriber = insert(:user)
User.subscribe(subscriber, user)
{:ok, status} = CommonAPI.post(user, %{status: "inwisible", visibility: "direct"})
assert {:ok, []} == Notification.create_notifications(status)
end
test "it disables notifications from people who are invisible" do
author = insert(:user, invisible: true)
user = insert(:user)
{:ok, status} = CommonAPI.post(author, %{status: "hey @#{user.nickname}"})
refute Notification.create_notification(status, user)
end
test "it doesn't create notifications if content matches with an irreversible filter" do
user = insert(:user)
subscriber = insert(:user)
User.subscribe(subscriber, user)
insert(:filter, user: subscriber, phrase: "cofe", hide: true)
{:ok, status} = CommonAPI.post(user, %{status: "got cofe?"})
assert {:ok, []} == Notification.create_notifications(status)
end
test "it creates notifications if content matches with a not irreversible filter" do
user = insert(:user)
subscriber = insert(:user)
User.subscribe(subscriber, user)
insert(:filter, user: subscriber, phrase: "cofe", hide: false)
{:ok, status} = CommonAPI.post(user, %{status: "got cofe?"})
{:ok, [notification]} = Notification.create_notifications(status)
assert notification
refute notification.seen
end
test "it creates notifications when someone likes user's status with a filtered word" do
user = insert(:user)
other_user = insert(:user)
insert(:filter, user: user, phrase: "tesla", hide: true)
{:ok, activity_one} = CommonAPI.post(user, %{status: "wow tesla"})
{:ok, activity_two} = CommonAPI.favorite(other_user, activity_one.id)
{:ok, [notification]} = Notification.create_notifications(activity_two)
assert notification
refute notification.seen
end
end
describe "follow / follow_request notifications" do
test "it creates `follow` notification for approved Follow activity" do
user = insert(:user)
followed_user = insert(:user, locked: false)
{:ok, _, _, _activity} = CommonAPI.follow(user, followed_user)
assert FollowingRelationship.following?(user, followed_user)
assert [notification] = Notification.for_user(followed_user)
assert %{type: "follow"} =
NotificationView.render("show.json", %{
notification: notification,
for: followed_user
})
end
test "it creates `follow_request` notification for pending Follow activity" do
user = insert(:user)
followed_user = insert(:user, locked: true)
{:ok, _, _, _activity} = CommonAPI.follow(user, followed_user)
refute FollowingRelationship.following?(user, followed_user)
assert [notification] = Notification.for_user(followed_user)
render_opts = %{notification: notification, for: followed_user}
assert %{type: "follow_request"} = NotificationView.render("show.json", render_opts)
# After request is accepted, the same notification is rendered with type "follow":
assert {:ok, _} = CommonAPI.accept_follow_request(user, followed_user)
notification =
Repo.get(Notification, notification.id)
|> Repo.preload(:activity)
assert %{type: "follow"} =
NotificationView.render("show.json", notification: notification, for: followed_user)
end
test "it doesn't create a notification for follow-unfollow-follow chains" do
user = insert(:user)
followed_user = insert(:user, locked: false)
{:ok, _, _, _activity} = CommonAPI.follow(user, followed_user)
assert FollowingRelationship.following?(user, followed_user)
assert [notification] = Notification.for_user(followed_user)
CommonAPI.unfollow(user, followed_user)
{:ok, _, _, _activity_dupe} = CommonAPI.follow(user, followed_user)
notification_id = notification.id
assert [%{id: ^notification_id}] = Notification.for_user(followed_user)
end
test "dismisses the notification on follow request rejection" do
user = insert(:user, locked: true)
follower = insert(:user)
{:ok, _, _, _follow_activity} = CommonAPI.follow(follower, user)
assert [notification] = Notification.for_user(user)
{:ok, _follower} = CommonAPI.reject_follow_request(follower, user)
assert [] = Notification.for_user(user)
end
end
describe "get notification" do
test "it gets a notification that belongs to the user" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "hey @#{other_user.nickname}"})
{:ok, [notification]} = Notification.create_notifications(activity)
{:ok, notification} = Notification.get(other_user, notification.id)
assert notification.user_id == other_user.id
end
test "it returns error if the notification doesn't belong to the user" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "hey @#{other_user.nickname}"})
{:ok, [notification]} = Notification.create_notifications(activity)
{:error, _notification} = Notification.get(user, notification.id)
end
end
describe "dismiss notification" do
test "it dismisses a notification that belongs to the user" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "hey @#{other_user.nickname}"})
{:ok, [notification]} = Notification.create_notifications(activity)
{:ok, notification} = Notification.dismiss(other_user, notification.id)
assert notification.user_id == other_user.id
end
test "it returns error if the notification doesn't belong to the user" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "hey @#{other_user.nickname}"})
{:ok, [notification]} = Notification.create_notifications(activity)
{:error, _notification} = Notification.dismiss(user, notification.id)
end
end
describe "clear notification" do
test "it clears all notifications belonging to the user" do
user = insert(:user)
other_user = insert(:user)
third_user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
status: "hey @#{other_user.nickname} and @#{third_user.nickname} !"
})
{:ok, _notifs} = Notification.create_notifications(activity)
{:ok, activity} =
CommonAPI.post(user, %{
status: "hey again @#{other_user.nickname} and @#{third_user.nickname} !"
})
{:ok, _notifs} = Notification.create_notifications(activity)
Notification.clear(other_user)
assert Notification.for_user(other_user) == []
assert Notification.for_user(third_user) != []
end
end
describe "set_read_up_to()" do
test "it sets all notifications as read up to a specified notification ID" do
user = insert(:user)
other_user = insert(:user)
{:ok, _activity} =
CommonAPI.post(user, %{
status: "hey @#{other_user.nickname}!"
})
{:ok, _activity} =
CommonAPI.post(user, %{
status: "hey again @#{other_user.nickname}!"
})
[n2, n1] = Notification.for_user(other_user)
assert n2.id > n1.id
{:ok, _activity} =
CommonAPI.post(user, %{
status: "hey yet again @#{other_user.nickname}!"
})
[_, read_notification] = Notification.set_read_up_to(other_user, n2.id)
assert read_notification.activity.object
[n3, n2, n1] = Notification.for_user(other_user)
assert n1.seen == true
assert n2.seen == true
assert n3.seen == false
assert %Pleroma.Marker{} =
m =
Pleroma.Repo.get_by(
Pleroma.Marker,
user_id: other_user.id,
timeline: "notifications"
)
assert m.last_read_id == to_string(n2.id)
end
end
describe "for_user_since/2" do
defp days_ago(days) do
NaiveDateTime.add(
NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second),
-days * 60 * 60 * 24,
:second
)
end
test "Returns recent notifications" do
user1 = insert(:user)
user2 = insert(:user)
Enum.each(0..10, fn i ->
{:ok, _activity} =
CommonAPI.post(user1, %{
status: "hey ##{i} @#{user2.nickname}!"
})
end)
{old, new} = Enum.split(Notification.for_user(user2), 5)
Enum.each(old, fn notification ->
notification
|> cast(%{updated_at: days_ago(10)}, [:updated_at])
|> Pleroma.Repo.update!()
end)
recent_notifications_ids =
user2
|> Notification.for_user_since(
NaiveDateTime.add(NaiveDateTime.utc_now(), -5 * 86_400, :second)
)
|> Enum.map(& &1.id)
Enum.each(old, fn %{id: id} ->
refute id in recent_notifications_ids
end)
Enum.each(new, fn %{id: id} ->
assert id in recent_notifications_ids
end)
end
end
describe "notification target determination / get_notified_from_activity/2" do
test "it sends notifications to addressed users in new messages" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
status: "hey @#{other_user.nickname}!"
})
{enabled_receivers, _disabled_receivers} = Notification.get_notified_from_activity(activity)
assert other_user in enabled_receivers
end
test "it sends notifications to mentioned users in new messages" do
user = insert(:user)
other_user = insert(:user)
create_activity = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"type" => "Create",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"actor" => user.ap_id,
"object" => %{
"type" => "Note",
"content" => "message with a Mention tag, but no explicit tagging",
"tag" => [
%{
"type" => "Mention",
"href" => other_user.ap_id,
"name" => other_user.nickname
}
],
"attributedTo" => user.ap_id
}
}
{:ok, activity} = Transmogrifier.handle_incoming(create_activity)
{enabled_receivers, _disabled_receivers} = Notification.get_notified_from_activity(activity)
assert other_user in enabled_receivers
end
test "it does not send notifications to users who are only cc in new messages" do
user = insert(:user)
other_user = insert(:user)
create_activity = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"type" => "Create",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [other_user.ap_id],
"actor" => user.ap_id,
"object" => %{
"type" => "Note",
"content" => "hi everyone",
"attributedTo" => user.ap_id
}
}
{:ok, activity} = Transmogrifier.handle_incoming(create_activity)
{enabled_receivers, _disabled_receivers} = Notification.get_notified_from_activity(activity)
assert other_user not in enabled_receivers
end
test "it does not send notification to mentioned users in likes" do
user = insert(:user)
other_user = insert(:user)
third_user = insert(:user)
{:ok, activity_one} =
CommonAPI.post(user, %{
status: "hey @#{other_user.nickname}!"
})
{:ok, activity_two} = CommonAPI.favorite(third_user, activity_one.id)
{enabled_receivers, _disabled_receivers} =
Notification.get_notified_from_activity(activity_two)
assert other_user not in enabled_receivers
end
test "it only notifies the post's author in likes" do
user = insert(:user)
other_user = insert(:user)
third_user = insert(:user)
{:ok, activity_one} =
CommonAPI.post(user, %{
status: "hey @#{other_user.nickname}!"
})
{:ok, like_data, _} = Builder.like(third_user, activity_one.object)
{:ok, like, _} =
like_data
|> Map.put("to", [other_user.ap_id | like_data["to"]])
|> ActivityPub.persist(local: true)
{enabled_receivers, _disabled_receivers} = Notification.get_notified_from_activity(like)
assert other_user not in enabled_receivers
end
test "it does not send notification to mentioned users in announces" do
user = insert(:user)
other_user = insert(:user)
third_user = insert(:user)
{:ok, activity_one} =
CommonAPI.post(user, %{
status: "hey @#{other_user.nickname}!"
})
{:ok, activity_two} = CommonAPI.repeat(activity_one.id, third_user)
{enabled_receivers, _disabled_receivers} =
Notification.get_notified_from_activity(activity_two)
assert other_user not in enabled_receivers
end
test "it returns blocking recipient in disabled recipients list" do
user = insert(:user)
other_user = insert(:user)
{:ok, _user_relationship} = User.block(other_user, user)
{:ok, activity} = CommonAPI.post(user, %{status: "hey @#{other_user.nickname}!"})
{enabled_receivers, disabled_receivers} = Notification.get_notified_from_activity(activity)
assert [] == enabled_receivers
assert [other_user] == disabled_receivers
end
test "it returns notification-muting recipient in disabled recipients list" do
user = insert(:user)
other_user = insert(:user)
{:ok, _user_relationships} = User.mute(other_user, user)
{:ok, activity} = CommonAPI.post(user, %{status: "hey @#{other_user.nickname}!"})
{enabled_receivers, disabled_receivers} = Notification.get_notified_from_activity(activity)
assert [] == enabled_receivers
assert [other_user] == disabled_receivers
end
test "it returns thread-muting recipient in disabled recipients list" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "hey @#{other_user.nickname}!"})
{:ok, _} = CommonAPI.add_mute(other_user, activity)
{:ok, same_context_activity} =
CommonAPI.post(user, %{
status: "hey-hey-hey @#{other_user.nickname}!",
in_reply_to_status_id: activity.id
})
{enabled_receivers, disabled_receivers} =
Notification.get_notified_from_activity(same_context_activity)
assert [other_user] == disabled_receivers
refute other_user in enabled_receivers
end
test "it returns non-following domain-blocking recipient in disabled recipients list" do
blocked_domain = "blocked.domain"
user = insert(:user, %{ap_id: "https://#{blocked_domain}/@actor"})
other_user = insert(:user)
{:ok, other_user} = User.block_domain(other_user, blocked_domain)
{:ok, activity} = CommonAPI.post(user, %{status: "hey @#{other_user.nickname}!"})
{enabled_receivers, disabled_receivers} = Notification.get_notified_from_activity(activity)
assert [] == enabled_receivers
assert [other_user] == disabled_receivers
end
test "it returns following domain-blocking recipient in enabled recipients list" do
blocked_domain = "blocked.domain"
user = insert(:user, %{ap_id: "https://#{blocked_domain}/@actor"})
other_user = insert(:user)
{:ok, other_user} = User.block_domain(other_user, blocked_domain)
{:ok, other_user} = User.follow(other_user, user)
{:ok, activity} = CommonAPI.post(user, %{status: "hey @#{other_user.nickname}!"})
{enabled_receivers, disabled_receivers} = Notification.get_notified_from_activity(activity)
assert [other_user] == enabled_receivers
assert [] == disabled_receivers
end
end
describe "notification lifecycle" do
test "liking an activity results in 1 notification, then 0 if the activity is deleted" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "test post"})
assert Enum.empty?(Notification.for_user(user))
{:ok, _} = CommonAPI.favorite(other_user, activity.id)
assert length(Notification.for_user(user)) == 1
{:ok, _} = CommonAPI.delete(activity.id, user)
assert Enum.empty?(Notification.for_user(user))
end
test "liking an activity results in 1 notification, then 0 if the activity is unliked" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "test post"})
assert Enum.empty?(Notification.for_user(user))
{:ok, _} = CommonAPI.favorite(other_user, activity.id)
assert length(Notification.for_user(user)) == 1
{:ok, _} = CommonAPI.unfavorite(activity.id, other_user)
assert Enum.empty?(Notification.for_user(user))
end
test "repeating an activity results in 1 notification, then 0 if the activity is deleted" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "test post"})
assert Enum.empty?(Notification.for_user(user))
{:ok, _} = CommonAPI.repeat(activity.id, other_user)
assert length(Notification.for_user(user)) == 1
{:ok, _} = CommonAPI.delete(activity.id, user)
assert Enum.empty?(Notification.for_user(user))
end
test "repeating an activity results in 1 notification, then 0 if the activity is unrepeated" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "test post"})
assert Enum.empty?(Notification.for_user(user))
{:ok, _} = CommonAPI.repeat(activity.id, other_user)
assert length(Notification.for_user(user)) == 1
{:ok, _} = CommonAPI.unrepeat(activity.id, other_user)
assert Enum.empty?(Notification.for_user(user))
end
test "liking an activity which is already deleted does not generate a notification" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "test post"})
assert Enum.empty?(Notification.for_user(user))
{:ok, _deletion_activity} = CommonAPI.delete(activity.id, user)
assert Enum.empty?(Notification.for_user(user))
{:error, :not_found} = CommonAPI.favorite(other_user, activity.id)
assert Enum.empty?(Notification.for_user(user))
end
test "repeating an activity which is already deleted does not generate a notification" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "test post"})
assert Enum.empty?(Notification.for_user(user))
{:ok, _deletion_activity} = CommonAPI.delete(activity.id, user)
assert Enum.empty?(Notification.for_user(user))
{:error, _} = CommonAPI.repeat(activity.id, other_user)
assert Enum.empty?(Notification.for_user(user))
end
test "replying to a deleted post without tagging does not generate a notification" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "test post"})
{:ok, _deletion_activity} = CommonAPI.delete(activity.id, user)
{:ok, _reply_activity} =
CommonAPI.post(other_user, %{
status: "test reply",
in_reply_to_status_id: activity.id
})
assert Enum.empty?(Notification.for_user(user))
end
test "notifications are deleted if a local user is deleted" do
user = insert(:user)
other_user = insert(:user)
{:ok, _activity} =
CommonAPI.post(user, %{status: "hi @#{other_user.nickname}", visibility: "direct"})
refute Enum.empty?(Notification.for_user(other_user))
{:ok, job} = User.delete(user)
ObanHelpers.perform(job)
assert Enum.empty?(Notification.for_user(other_user))
end
test "notifications are deleted if a remote user is deleted" do
remote_user = insert(:user)
local_user = insert(:user)
dm_message = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"type" => "Create",
"actor" => remote_user.ap_id,
"id" => remote_user.ap_id <> "/activities/test",
"to" => [local_user.ap_id],
"cc" => [],
"object" => %{
"type" => "Note",
"content" => "Hello!",
"tag" => [
%{
"type" => "Mention",
"href" => local_user.ap_id,
"name" => "@#{local_user.nickname}"
}
],
"to" => [local_user.ap_id],
"cc" => [],
"attributedTo" => remote_user.ap_id
}
}
{:ok, _dm_activity} = Transmogrifier.handle_incoming(dm_message)
refute Enum.empty?(Notification.for_user(local_user))
delete_user_message = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"id" => remote_user.ap_id <> "/activities/delete",
"actor" => remote_user.ap_id,
"type" => "Delete",
"object" => remote_user.ap_id
}
remote_user_url = remote_user.ap_id
Tesla.Mock.mock(fn
%{method: :get, url: ^remote_user_url} ->
%Tesla.Env{status: 404, body: ""}
end)
{:ok, _delete_activity} = Transmogrifier.handle_incoming(delete_user_message)
ObanHelpers.perform_all()
assert Enum.empty?(Notification.for_user(local_user))
end
@tag capture_log: true
test "move activity generates a notification" do
%{ap_id: old_ap_id} = old_user = insert(:user)
%{ap_id: new_ap_id} = new_user = insert(:user, also_known_as: [old_ap_id])
follower = insert(:user)
other_follower = insert(:user, %{allow_following_move: false})
User.follow(follower, old_user)
User.follow(other_follower, old_user)
old_user_url = old_user.ap_id
body =
File.read!("test/fixtures/users_mock/localhost.json")
|> String.replace("{{nickname}}", old_user.nickname)
|> Jason.encode!()
Tesla.Mock.mock(fn
%{method: :get, url: ^old_user_url} ->
%Tesla.Env{status: 200, body: body}
end)
Pleroma.Web.ActivityPub.ActivityPub.move(old_user, new_user)
ObanHelpers.perform_all()
assert [
%{
activity: %{
data: %{"type" => "Move", "actor" => ^old_ap_id, "target" => ^new_ap_id}
}
}
] = Notification.for_user(follower)
assert [
%{
activity: %{
data: %{"type" => "Move", "actor" => ^old_ap_id, "target" => ^new_ap_id}
}
}
] = Notification.for_user(other_follower)
end
end
describe "for_user" do
setup do
user = insert(:user)
{:ok, %{user: user}}
end
test "it returns notifications for muted user without notifications", %{user: user} do
muted = insert(:user)
- {:ok, _user_relationships} = User.mute(user, muted, false)
+ {:ok, _user_relationships} = User.mute(user, muted, %{notifications: false})
{:ok, _activity} = CommonAPI.post(muted, %{status: "hey @#{user.nickname}"})
[notification] = Notification.for_user(user)
assert notification.activity.object
assert notification.seen
end
test "it doesn't return notifications for muted user with notifications", %{user: user} do
muted = insert(:user)
{:ok, _user_relationships} = User.mute(user, muted)
{:ok, _activity} = CommonAPI.post(muted, %{status: "hey @#{user.nickname}"})
assert Notification.for_user(user) == []
end
test "it doesn't return notifications for blocked user", %{user: user} do
blocked = insert(:user)
{:ok, _user_relationship} = User.block(user, blocked)
{:ok, _activity} = CommonAPI.post(blocked, %{status: "hey @#{user.nickname}"})
assert Notification.for_user(user) == []
end
test "it doesn't return notifications for domain-blocked non-followed user", %{user: user} do
blocked = insert(:user, ap_id: "http://some-domain.com")
{:ok, user} = User.block_domain(user, "some-domain.com")
{:ok, _activity} = CommonAPI.post(blocked, %{status: "hey @#{user.nickname}"})
assert Notification.for_user(user) == []
end
test "it returns notifications for domain-blocked but followed user" do
user = insert(:user)
blocked = insert(:user, ap_id: "http://some-domain.com")
{:ok, user} = User.block_domain(user, "some-domain.com")
{:ok, _} = User.follow(user, blocked)
{:ok, _activity} = CommonAPI.post(blocked, %{status: "hey @#{user.nickname}"})
assert length(Notification.for_user(user)) == 1
end
test "it doesn't return notifications for muted thread", %{user: user} do
another_user = insert(:user)
{:ok, activity} = CommonAPI.post(another_user, %{status: "hey @#{user.nickname}"})
{:ok, _} = Pleroma.ThreadMute.add_mute(user.id, activity.data["context"])
assert Notification.for_user(user) == []
end
test "it returns notifications from a muted user when with_muted is set", %{user: user} do
muted = insert(:user)
{:ok, _user_relationships} = User.mute(user, muted)
{:ok, _activity} = CommonAPI.post(muted, %{status: "hey @#{user.nickname}"})
assert length(Notification.for_user(user, %{with_muted: true})) == 1
end
test "it doesn't return notifications from a blocked user when with_muted is set", %{
user: user
} do
blocked = insert(:user)
{:ok, _user_relationship} = User.block(user, blocked)
{:ok, _activity} = CommonAPI.post(blocked, %{status: "hey @#{user.nickname}"})
assert Enum.empty?(Notification.for_user(user, %{with_muted: true}))
end
test "when with_muted is set, " <>
"it doesn't return notifications from a domain-blocked non-followed user",
%{user: user} do
blocked = insert(:user, ap_id: "http://some-domain.com")
{:ok, user} = User.block_domain(user, "some-domain.com")
{:ok, _activity} = CommonAPI.post(blocked, %{status: "hey @#{user.nickname}"})
assert Enum.empty?(Notification.for_user(user, %{with_muted: true}))
end
test "it returns notifications from muted threads when with_muted is set", %{user: user} do
another_user = insert(:user)
{:ok, activity} = CommonAPI.post(another_user, %{status: "hey @#{user.nickname}"})
{:ok, _} = Pleroma.ThreadMute.add_mute(user.id, activity.data["context"])
assert length(Notification.for_user(user, %{with_muted: true})) == 1
end
test "it doesn't return notifications about mentions with filtered word", %{user: user} do
insert(:filter, user: user, phrase: "cofe", hide: true)
another_user = insert(:user)
{:ok, _activity} = CommonAPI.post(another_user, %{status: "@#{user.nickname} got cofe?"})
assert Enum.empty?(Notification.for_user(user))
end
test "it returns notifications about mentions with not hidden filtered word", %{user: user} do
insert(:filter, user: user, phrase: "test", hide: false)
another_user = insert(:user)
{:ok, _} = CommonAPI.post(another_user, %{status: "@#{user.nickname} test"})
assert length(Notification.for_user(user)) == 1
end
test "it returns notifications about favorites with filtered word", %{user: user} do
insert(:filter, user: user, phrase: "cofe", hide: true)
another_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "Give me my cofe!"})
{:ok, _} = CommonAPI.favorite(another_user, activity.id)
assert length(Notification.for_user(user)) == 1
end
end
end
diff --git a/test/user_test.exs b/test/user_test.exs
index 50f72549e..b23e36be3 100644
--- a/test/user_test.exs
+++ b/test/user_test.exs
@@ -1,2105 +1,2105 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.UserTest do
alias Pleroma.Activity
alias Pleroma.Builders.UserBuilder
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.CommonAPI
use Pleroma.DataCase
use Oban.Testing, repo: Pleroma.Repo
import Pleroma.Factory
import ExUnit.CaptureLog
import Swoosh.TestAssertions
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
setup do: clear_config([:instance, :account_activation_required])
describe "service actors" do
test "returns updated invisible actor" do
uri = "#{Pleroma.Web.Endpoint.url()}/relay"
followers_uri = "#{uri}/followers"
insert(
:user,
%{
nickname: "relay",
invisible: false,
local: true,
ap_id: uri,
follower_address: followers_uri
}
)
actor = User.get_or_create_service_actor_by_ap_id(uri, "relay")
assert actor.invisible
end
test "returns relay user" do
uri = "#{Pleroma.Web.Endpoint.url()}/relay"
followers_uri = "#{uri}/followers"
assert %User{
nickname: "relay",
invisible: true,
local: true,
ap_id: ^uri,
follower_address: ^followers_uri
} = User.get_or_create_service_actor_by_ap_id(uri, "relay")
assert capture_log(fn ->
refute User.get_or_create_service_actor_by_ap_id("/relay", "relay")
end) =~ "Cannot create service actor:"
end
test "returns invisible actor" do
uri = "#{Pleroma.Web.Endpoint.url()}/internal/fetch-test"
followers_uri = "#{uri}/followers"
user = User.get_or_create_service_actor_by_ap_id(uri, "internal.fetch-test")
assert %User{
nickname: "internal.fetch-test",
invisible: true,
local: true,
ap_id: ^uri,
follower_address: ^followers_uri
} = user
user2 = User.get_or_create_service_actor_by_ap_id(uri, "internal.fetch-test")
assert user.id == user2.id
end
end
describe "AP ID user relationships" do
setup do
{:ok, user: insert(:user)}
end
test "outgoing_relationships_ap_ids/1", %{user: user} do
rel_types = [:block, :mute, :notification_mute, :reblog_mute, :inverse_subscription]
ap_ids_by_rel =
Enum.into(
rel_types,
%{},
fn rel_type ->
rel_records =
insert_list(2, :user_relationship, %{source: user, relationship_type: rel_type})
ap_ids = Enum.map(rel_records, fn rr -> Repo.preload(rr, :target).target.ap_id end)
{rel_type, Enum.sort(ap_ids)}
end
)
assert ap_ids_by_rel[:block] == Enum.sort(User.blocked_users_ap_ids(user))
assert ap_ids_by_rel[:block] == Enum.sort(Enum.map(User.blocked_users(user), & &1.ap_id))
assert ap_ids_by_rel[:mute] == Enum.sort(User.muted_users_ap_ids(user))
assert ap_ids_by_rel[:mute] == Enum.sort(Enum.map(User.muted_users(user), & &1.ap_id))
assert ap_ids_by_rel[:notification_mute] ==
Enum.sort(User.notification_muted_users_ap_ids(user))
assert ap_ids_by_rel[:notification_mute] ==
Enum.sort(Enum.map(User.notification_muted_users(user), & &1.ap_id))
assert ap_ids_by_rel[:reblog_mute] == Enum.sort(User.reblog_muted_users_ap_ids(user))
assert ap_ids_by_rel[:reblog_mute] ==
Enum.sort(Enum.map(User.reblog_muted_users(user), & &1.ap_id))
assert ap_ids_by_rel[:inverse_subscription] == Enum.sort(User.subscriber_users_ap_ids(user))
assert ap_ids_by_rel[:inverse_subscription] ==
Enum.sort(Enum.map(User.subscriber_users(user), & &1.ap_id))
outgoing_relationships_ap_ids = User.outgoing_relationships_ap_ids(user, rel_types)
assert ap_ids_by_rel ==
Enum.into(outgoing_relationships_ap_ids, %{}, fn {k, v} -> {k, Enum.sort(v)} end)
end
end
describe "when tags are nil" do
test "tagging a user" do
user = insert(:user, %{tags: nil})
user = User.tag(user, ["cool", "dude"])
assert "cool" in user.tags
assert "dude" in user.tags
end
test "untagging a user" do
user = insert(:user, %{tags: nil})
user = User.untag(user, ["cool", "dude"])
assert user.tags == []
end
end
test "ap_id returns the activity pub id for the user" do
user = UserBuilder.build()
expected_ap_id = "#{Pleroma.Web.base_url()}/users/#{user.nickname}"
assert expected_ap_id == User.ap_id(user)
end
test "ap_followers returns the followers collection for the user" do
user = UserBuilder.build()
expected_followers_collection = "#{User.ap_id(user)}/followers"
assert expected_followers_collection == User.ap_followers(user)
end
test "ap_following returns the following collection for the user" do
user = UserBuilder.build()
expected_followers_collection = "#{User.ap_id(user)}/following"
assert expected_followers_collection == User.ap_following(user)
end
test "returns all pending follow requests" do
unlocked = insert(:user)
locked = insert(:user, locked: true)
follower = insert(:user)
CommonAPI.follow(follower, unlocked)
CommonAPI.follow(follower, locked)
assert [] = User.get_follow_requests(unlocked)
assert [activity] = User.get_follow_requests(locked)
assert activity
end
test "doesn't return already accepted or duplicate follow requests" do
locked = insert(:user, locked: true)
pending_follower = insert(:user)
accepted_follower = insert(:user)
CommonAPI.follow(pending_follower, locked)
CommonAPI.follow(pending_follower, locked)
CommonAPI.follow(accepted_follower, locked)
Pleroma.FollowingRelationship.update(accepted_follower, locked, :follow_accept)
assert [^pending_follower] = User.get_follow_requests(locked)
end
test "doesn't return follow requests for deactivated accounts" do
locked = insert(:user, locked: true)
pending_follower = insert(:user, %{deactivated: true})
CommonAPI.follow(pending_follower, locked)
assert true == pending_follower.deactivated
assert [] = User.get_follow_requests(locked)
end
test "clears follow requests when requester is blocked" do
followed = insert(:user, locked: true)
follower = insert(:user)
CommonAPI.follow(follower, followed)
assert [_activity] = User.get_follow_requests(followed)
{:ok, _user_relationship} = User.block(followed, follower)
assert [] = User.get_follow_requests(followed)
end
test "follow_all follows mutliple users" do
user = insert(:user)
followed_zero = insert(:user)
followed_one = insert(:user)
followed_two = insert(:user)
blocked = insert(:user)
not_followed = insert(:user)
reverse_blocked = insert(:user)
{:ok, _user_relationship} = User.block(user, blocked)
{:ok, _user_relationship} = User.block(reverse_blocked, user)
{:ok, user} = User.follow(user, followed_zero)
{:ok, user} = User.follow_all(user, [followed_one, followed_two, blocked, reverse_blocked])
assert User.following?(user, followed_one)
assert User.following?(user, followed_two)
assert User.following?(user, followed_zero)
refute User.following?(user, not_followed)
refute User.following?(user, blocked)
refute User.following?(user, reverse_blocked)
end
test "follow_all follows mutliple users without duplicating" do
user = insert(:user)
followed_zero = insert(:user)
followed_one = insert(:user)
followed_two = insert(:user)
{:ok, user} = User.follow_all(user, [followed_zero, followed_one])
assert length(User.following(user)) == 3
{:ok, user} = User.follow_all(user, [followed_one, followed_two])
assert length(User.following(user)) == 4
end
test "follow takes a user and another user" do
user = insert(:user)
followed = insert(:user)
{:ok, user} = User.follow(user, followed)
user = User.get_cached_by_id(user.id)
followed = User.get_cached_by_ap_id(followed.ap_id)
assert followed.follower_count == 1
assert user.following_count == 1
assert User.ap_followers(followed) in User.following(user)
end
test "can't follow a deactivated users" do
user = insert(:user)
followed = insert(:user, %{deactivated: true})
{:error, _} = User.follow(user, followed)
end
test "can't follow a user who blocked us" do
blocker = insert(:user)
blockee = insert(:user)
{:ok, _user_relationship} = User.block(blocker, blockee)
{:error, _} = User.follow(blockee, blocker)
end
test "can't subscribe to a user who blocked us" do
blocker = insert(:user)
blocked = insert(:user)
{:ok, _user_relationship} = User.block(blocker, blocked)
{:error, _} = User.subscribe(blocked, blocker)
end
test "local users do not automatically follow local locked accounts" do
follower = insert(:user, locked: true)
followed = insert(:user, locked: true)
{:ok, follower} = User.maybe_direct_follow(follower, followed)
refute User.following?(follower, followed)
end
describe "unfollow/2" do
setup do: clear_config([:instance, :external_user_synchronization])
test "unfollow with syncronizes external user" do
Pleroma.Config.put([:instance, :external_user_synchronization], true)
followed =
insert(:user,
nickname: "fuser1",
follower_address: "http://localhost:4001/users/fuser1/followers",
following_address: "http://localhost:4001/users/fuser1/following",
ap_id: "http://localhost:4001/users/fuser1"
)
user =
insert(:user, %{
local: false,
nickname: "fuser2",
ap_id: "http://localhost:4001/users/fuser2",
follower_address: "http://localhost:4001/users/fuser2/followers",
following_address: "http://localhost:4001/users/fuser2/following"
})
{:ok, user} = User.follow(user, followed, :follow_accept)
{:ok, user, _activity} = User.unfollow(user, followed)
user = User.get_cached_by_id(user.id)
assert User.following(user) == []
end
test "unfollow takes a user and another user" do
followed = insert(:user)
user = insert(:user)
{:ok, user} = User.follow(user, followed, :follow_accept)
assert User.following(user) == [user.follower_address, followed.follower_address]
{:ok, user, _activity} = User.unfollow(user, followed)
assert User.following(user) == [user.follower_address]
end
test "unfollow doesn't unfollow yourself" do
user = insert(:user)
{:error, _} = User.unfollow(user, user)
assert User.following(user) == [user.follower_address]
end
end
test "test if a user is following another user" do
followed = insert(:user)
user = insert(:user)
User.follow(user, followed, :follow_accept)
assert User.following?(user, followed)
refute User.following?(followed, user)
end
test "fetches correct profile for nickname beginning with number" do
# Use old-style integer ID to try to reproduce the problem
user = insert(:user, %{id: 1080})
user_with_numbers = insert(:user, %{nickname: "#{user.id}garbage"})
assert user_with_numbers == User.get_cached_by_nickname_or_id(user_with_numbers.nickname)
end
describe "user registration" do
@full_user_data %{
bio: "A guy",
name: "my name",
nickname: "nick",
password: "test",
password_confirmation: "test",
email: "email@example.com"
}
setup do: clear_config([:instance, :autofollowed_nicknames])
setup do: clear_config([:welcome])
setup do: clear_config([:instance, :account_activation_required])
test "it autofollows accounts that are set for it" do
user = insert(:user)
remote_user = insert(:user, %{local: false})
Pleroma.Config.put([:instance, :autofollowed_nicknames], [
user.nickname,
remote_user.nickname
])
cng = User.register_changeset(%User{}, @full_user_data)
{:ok, registered_user} = User.register(cng)
assert User.following?(registered_user, user)
refute User.following?(registered_user, remote_user)
end
test "it sends a welcome message if it is set" do
welcome_user = insert(:user)
Pleroma.Config.put([:welcome, :direct_message, :enabled], true)
Pleroma.Config.put([:welcome, :direct_message, :sender_nickname], welcome_user.nickname)
Pleroma.Config.put([:welcome, :direct_message, :message], "Hello, this is a direct message")
cng = User.register_changeset(%User{}, @full_user_data)
{:ok, registered_user} = User.register(cng)
ObanHelpers.perform_all()
activity = Repo.one(Pleroma.Activity)
assert registered_user.ap_id in activity.recipients
assert Object.normalize(activity).data["content"] =~ "direct message"
assert activity.actor == welcome_user.ap_id
end
test "it sends a welcome chat message if it is set" do
welcome_user = insert(:user)
Pleroma.Config.put([:welcome, :chat_message, :enabled], true)
Pleroma.Config.put([:welcome, :chat_message, :sender_nickname], welcome_user.nickname)
Pleroma.Config.put([:welcome, :chat_message, :message], "Hello, this is a chat message")
cng = User.register_changeset(%User{}, @full_user_data)
{:ok, registered_user} = User.register(cng)
ObanHelpers.perform_all()
activity = Repo.one(Pleroma.Activity)
assert registered_user.ap_id in activity.recipients
assert Object.normalize(activity).data["content"] =~ "chat message"
assert activity.actor == welcome_user.ap_id
end
test "it sends a welcome email message if it is set" do
welcome_user = insert(:user)
Pleroma.Config.put([:welcome, :email, :enabled], true)
Pleroma.Config.put([:welcome, :email, :sender], welcome_user.email)
Pleroma.Config.put(
[:welcome, :email, :subject],
"Hello, welcome to cool site: <%= instance_name %>"
)
instance_name = Pleroma.Config.get([:instance, :name])
cng = User.register_changeset(%User{}, @full_user_data)
{:ok, registered_user} = User.register(cng)
ObanHelpers.perform_all()
assert_email_sent(
from: {instance_name, welcome_user.email},
to: {registered_user.name, registered_user.email},
subject: "Hello, welcome to cool site: #{instance_name}",
html_body: "Welcome to #{instance_name}"
)
end
test "it sends a confirm email" do
Pleroma.Config.put([:instance, :account_activation_required], true)
cng = User.register_changeset(%User{}, @full_user_data)
{:ok, registered_user} = User.register(cng)
ObanHelpers.perform_all()
assert_email_sent(Pleroma.Emails.UserEmail.account_confirmation_email(registered_user))
end
test "it requires an email, name, nickname and password, bio is optional when account_activation_required is enabled" do
Pleroma.Config.put([:instance, :account_activation_required], true)
@full_user_data
|> Map.keys()
|> Enum.each(fn key ->
params = Map.delete(@full_user_data, key)
changeset = User.register_changeset(%User{}, params)
assert if key == :bio, do: changeset.valid?, else: not changeset.valid?
end)
end
test "it requires an name, nickname and password, bio and email are optional when account_activation_required is disabled" do
Pleroma.Config.put([:instance, :account_activation_required], false)
@full_user_data
|> Map.keys()
|> Enum.each(fn key ->
params = Map.delete(@full_user_data, key)
changeset = User.register_changeset(%User{}, params)
assert if key in [:bio, :email], do: changeset.valid?, else: not changeset.valid?
end)
end
test "it restricts certain nicknames" do
[restricted_name | _] = Pleroma.Config.get([User, :restricted_nicknames])
assert is_bitstring(restricted_name)
params =
@full_user_data
|> Map.put(:nickname, restricted_name)
changeset = User.register_changeset(%User{}, params)
refute changeset.valid?
end
test "it blocks blacklisted email domains" do
clear_config([User, :email_blacklist], ["trolling.world"])
# Block with match
params = Map.put(@full_user_data, :email, "troll@trolling.world")
changeset = User.register_changeset(%User{}, params)
refute changeset.valid?
# Block with subdomain match
params = Map.put(@full_user_data, :email, "troll@gnomes.trolling.world")
changeset = User.register_changeset(%User{}, params)
refute changeset.valid?
# Pass with different domains that are similar
params = Map.put(@full_user_data, :email, "troll@gnomestrolling.world")
changeset = User.register_changeset(%User{}, params)
assert changeset.valid?
params = Map.put(@full_user_data, :email, "troll@trolling.world.us")
changeset = User.register_changeset(%User{}, params)
assert changeset.valid?
end
test "it sets the password_hash and ap_id" do
changeset = User.register_changeset(%User{}, @full_user_data)
assert changeset.valid?
assert is_binary(changeset.changes[:password_hash])
assert changeset.changes[:ap_id] == User.ap_id(%User{nickname: @full_user_data.nickname})
assert changeset.changes.follower_address == "#{changeset.changes.ap_id}/followers"
end
test "it sets the 'accepts_chat_messages' set to true" do
changeset = User.register_changeset(%User{}, @full_user_data)
assert changeset.valid?
{:ok, user} = Repo.insert(changeset)
assert user.accepts_chat_messages
end
test "it creates a confirmed user" do
changeset = User.register_changeset(%User{}, @full_user_data)
assert changeset.valid?
{:ok, user} = Repo.insert(changeset)
refute user.confirmation_pending
end
end
describe "user registration, with :account_activation_required" do
@full_user_data %{
bio: "A guy",
name: "my name",
nickname: "nick",
password: "test",
password_confirmation: "test",
email: "email@example.com"
}
setup do: clear_config([:instance, :account_activation_required], true)
test "it creates unconfirmed user" do
changeset = User.register_changeset(%User{}, @full_user_data)
assert changeset.valid?
{:ok, user} = Repo.insert(changeset)
assert user.confirmation_pending
assert user.confirmation_token
end
test "it creates confirmed user if :confirmed option is given" do
changeset = User.register_changeset(%User{}, @full_user_data, need_confirmation: false)
assert changeset.valid?
{:ok, user} = Repo.insert(changeset)
refute user.confirmation_pending
refute user.confirmation_token
end
end
describe "user registration, with :account_approval_required" do
@full_user_data %{
bio: "A guy",
name: "my name",
nickname: "nick",
password: "test",
password_confirmation: "test",
email: "email@example.com",
registration_reason: "I'm a cool guy :)"
}
setup do: clear_config([:instance, :account_approval_required], true)
test "it creates unapproved user" do
changeset = User.register_changeset(%User{}, @full_user_data)
assert changeset.valid?
{:ok, user} = Repo.insert(changeset)
assert user.approval_pending
assert user.registration_reason == "I'm a cool guy :)"
end
test "it restricts length of registration reason" do
reason_limit = Pleroma.Config.get([:instance, :registration_reason_length])
assert is_integer(reason_limit)
params =
@full_user_data
|> Map.put(
:registration_reason,
"Quia et nesciunt dolores numquam ipsam nisi sapiente soluta. Ullam repudiandae nisi quam porro officiis officiis ad. Consequatur animi velit ex quia. Odit voluptatem perferendis quia ut nisi. Dignissimos sit soluta atque aliquid dolorem ut dolorum ut. Labore voluptates iste iusto amet voluptatum earum. Ad fugit illum nam eos ut nemo. Pariatur ea fuga non aspernatur. Dignissimos debitis officia corporis est nisi ab et. Atque itaque alias eius voluptas minus. Accusamus numquam tempore occaecati in."
)
changeset = User.register_changeset(%User{}, params)
refute changeset.valid?
end
end
describe "get_or_fetch/1" do
test "gets an existing user by nickname" do
user = insert(:user)
{:ok, fetched_user} = User.get_or_fetch(user.nickname)
assert user == fetched_user
end
test "gets an existing user by ap_id" do
ap_id = "http://mastodon.example.org/users/admin"
user =
insert(
:user,
local: false,
nickname: "admin@mastodon.example.org",
ap_id: ap_id
)
{:ok, fetched_user} = User.get_or_fetch(ap_id)
freshed_user = refresh_record(user)
assert freshed_user == fetched_user
end
end
describe "fetching a user from nickname or trying to build one" do
test "gets an existing user" do
user = insert(:user)
{:ok, fetched_user} = User.get_or_fetch_by_nickname(user.nickname)
assert user == fetched_user
end
test "gets an existing user, case insensitive" do
user = insert(:user, nickname: "nick")
{:ok, fetched_user} = User.get_or_fetch_by_nickname("NICK")
assert user == fetched_user
end
test "gets an existing user by fully qualified nickname" do
user = insert(:user)
{:ok, fetched_user} =
User.get_or_fetch_by_nickname(user.nickname <> "@" <> Pleroma.Web.Endpoint.host())
assert user == fetched_user
end
test "gets an existing user by fully qualified nickname, case insensitive" do
user = insert(:user, nickname: "nick")
casing_altered_fqn = String.upcase(user.nickname <> "@" <> Pleroma.Web.Endpoint.host())
{:ok, fetched_user} = User.get_or_fetch_by_nickname(casing_altered_fqn)
assert user == fetched_user
end
@tag capture_log: true
test "returns nil if no user could be fetched" do
{:error, fetched_user} = User.get_or_fetch_by_nickname("nonexistant@social.heldscal.la")
assert fetched_user == "not found nonexistant@social.heldscal.la"
end
test "returns nil for nonexistant local user" do
{:error, fetched_user} = User.get_or_fetch_by_nickname("nonexistant")
assert fetched_user == "not found nonexistant"
end
test "updates an existing user, if stale" do
a_week_ago = NaiveDateTime.add(NaiveDateTime.utc_now(), -604_800)
orig_user =
insert(
:user,
local: false,
nickname: "admin@mastodon.example.org",
ap_id: "http://mastodon.example.org/users/admin",
last_refreshed_at: a_week_ago
)
assert orig_user.last_refreshed_at == a_week_ago
{:ok, user} = User.get_or_fetch_by_ap_id("http://mastodon.example.org/users/admin")
assert user.inbox
refute user.last_refreshed_at == orig_user.last_refreshed_at
end
test "if nicknames clash, the old user gets a prefix with the old id to the nickname" do
a_week_ago = NaiveDateTime.add(NaiveDateTime.utc_now(), -604_800)
orig_user =
insert(
:user,
local: false,
nickname: "admin@mastodon.example.org",
ap_id: "http://mastodon.example.org/users/harinezumigari",
last_refreshed_at: a_week_ago
)
assert orig_user.last_refreshed_at == a_week_ago
{:ok, user} = User.get_or_fetch_by_ap_id("http://mastodon.example.org/users/admin")
assert user.inbox
refute user.id == orig_user.id
orig_user = User.get_by_id(orig_user.id)
assert orig_user.nickname == "#{orig_user.id}.admin@mastodon.example.org"
end
@tag capture_log: true
test "it returns the old user if stale, but unfetchable" do
a_week_ago = NaiveDateTime.add(NaiveDateTime.utc_now(), -604_800)
orig_user =
insert(
:user,
local: false,
nickname: "admin@mastodon.example.org",
ap_id: "http://mastodon.example.org/users/raymoo",
last_refreshed_at: a_week_ago
)
assert orig_user.last_refreshed_at == a_week_ago
{:ok, user} = User.get_or_fetch_by_ap_id("http://mastodon.example.org/users/raymoo")
assert user.last_refreshed_at == orig_user.last_refreshed_at
end
end
test "returns an ap_id for a user" do
user = insert(:user)
assert User.ap_id(user) ==
Pleroma.Web.Router.Helpers.user_feed_url(
Pleroma.Web.Endpoint,
:feed_redirect,
user.nickname
)
end
test "returns an ap_followers link for a user" do
user = insert(:user)
assert User.ap_followers(user) ==
Pleroma.Web.Router.Helpers.user_feed_url(
Pleroma.Web.Endpoint,
:feed_redirect,
user.nickname
) <> "/followers"
end
describe "remote user changeset" do
@valid_remote %{
bio: "hello",
name: "Someone",
nickname: "a@b.de",
ap_id: "http...",
avatar: %{some: "avatar"}
}
setup do: clear_config([:instance, :user_bio_length])
setup do: clear_config([:instance, :user_name_length])
test "it confirms validity" do
cs = User.remote_user_changeset(@valid_remote)
assert cs.valid?
end
test "it sets the follower_adress" do
cs = User.remote_user_changeset(@valid_remote)
# remote users get a fake local follower address
assert cs.changes.follower_address ==
User.ap_followers(%User{nickname: @valid_remote[:nickname]})
end
test "it enforces the fqn format for nicknames" do
cs = User.remote_user_changeset(%{@valid_remote | nickname: "bla"})
assert Ecto.Changeset.get_field(cs, :local) == false
assert cs.changes.avatar
refute cs.valid?
end
test "it has required fields" do
[:ap_id]
|> Enum.each(fn field ->
cs = User.remote_user_changeset(Map.delete(@valid_remote, field))
refute cs.valid?
end)
end
end
describe "followers and friends" do
test "gets all followers for a given user" do
user = insert(:user)
follower_one = insert(:user)
follower_two = insert(:user)
not_follower = insert(:user)
{:ok, follower_one} = User.follow(follower_one, user)
{:ok, follower_two} = User.follow(follower_two, user)
res = User.get_followers(user)
assert Enum.member?(res, follower_one)
assert Enum.member?(res, follower_two)
refute Enum.member?(res, not_follower)
end
test "gets all friends (followed users) for a given user" do
user = insert(:user)
followed_one = insert(:user)
followed_two = insert(:user)
not_followed = insert(:user)
{:ok, user} = User.follow(user, followed_one)
{:ok, user} = User.follow(user, followed_two)
res = User.get_friends(user)
followed_one = User.get_cached_by_ap_id(followed_one.ap_id)
followed_two = User.get_cached_by_ap_id(followed_two.ap_id)
assert Enum.member?(res, followed_one)
assert Enum.member?(res, followed_two)
refute Enum.member?(res, not_followed)
end
end
describe "updating note and follower count" do
test "it sets the note_count property" do
note = insert(:note)
user = User.get_cached_by_ap_id(note.data["actor"])
assert user.note_count == 0
{:ok, user} = User.update_note_count(user)
assert user.note_count == 1
end
test "it increases the note_count property" do
note = insert(:note)
user = User.get_cached_by_ap_id(note.data["actor"])
assert user.note_count == 0
{:ok, user} = User.increase_note_count(user)
assert user.note_count == 1
{:ok, user} = User.increase_note_count(user)
assert user.note_count == 2
end
test "it decreases the note_count property" do
note = insert(:note)
user = User.get_cached_by_ap_id(note.data["actor"])
assert user.note_count == 0
{:ok, user} = User.increase_note_count(user)
assert user.note_count == 1
{:ok, user} = User.decrease_note_count(user)
assert user.note_count == 0
{:ok, user} = User.decrease_note_count(user)
assert user.note_count == 0
end
test "it sets the follower_count property" do
user = insert(:user)
follower = insert(:user)
User.follow(follower, user)
assert user.follower_count == 0
{:ok, user} = User.update_follower_count(user)
assert user.follower_count == 1
end
end
describe "follow_import" do
test "it imports user followings from list" do
[user1, user2, user3] = insert_list(3, :user)
identifiers = [
user2.ap_id,
user3.nickname
]
{:ok, job} = User.follow_import(user1, identifiers)
assert {:ok, result} = ObanHelpers.perform(job)
assert is_list(result)
assert result == [user2, user3]
end
end
describe "mutes" do
test "it mutes people" do
user = insert(:user)
muted_user = insert(:user)
refute User.mutes?(user, muted_user)
refute User.muted_notifications?(user, muted_user)
{:ok, _user_relationships} = User.mute(user, muted_user)
assert User.mutes?(user, muted_user)
assert User.muted_notifications?(user, muted_user)
end
test "it unmutes users" do
user = insert(:user)
muted_user = insert(:user)
{:ok, _user_relationships} = User.mute(user, muted_user)
{:ok, _user_mute} = User.unmute(user, muted_user)
refute User.mutes?(user, muted_user)
refute User.muted_notifications?(user, muted_user)
end
test "it mutes user without notifications" do
user = insert(:user)
muted_user = insert(:user)
refute User.mutes?(user, muted_user)
refute User.muted_notifications?(user, muted_user)
- {:ok, _user_relationships} = User.mute(user, muted_user, false)
+ {:ok, _user_relationships} = User.mute(user, muted_user, %{notifications: false})
assert User.mutes?(user, muted_user)
refute User.muted_notifications?(user, muted_user)
end
end
describe "blocks" do
test "it blocks people" do
user = insert(:user)
blocked_user = insert(:user)
refute User.blocks?(user, blocked_user)
{:ok, _user_relationship} = User.block(user, blocked_user)
assert User.blocks?(user, blocked_user)
end
test "it unblocks users" do
user = insert(:user)
blocked_user = insert(:user)
{:ok, _user_relationship} = User.block(user, blocked_user)
{:ok, _user_block} = User.unblock(user, blocked_user)
refute User.blocks?(user, blocked_user)
end
test "blocks tear down cyclical follow relationships" do
blocker = insert(:user)
blocked = insert(:user)
{:ok, blocker} = User.follow(blocker, blocked)
{:ok, blocked} = User.follow(blocked, blocker)
assert User.following?(blocker, blocked)
assert User.following?(blocked, blocker)
{:ok, _user_relationship} = User.block(blocker, blocked)
blocked = User.get_cached_by_id(blocked.id)
assert User.blocks?(blocker, blocked)
refute User.following?(blocker, blocked)
refute User.following?(blocked, blocker)
end
test "blocks tear down blocker->blocked follow relationships" do
blocker = insert(:user)
blocked = insert(:user)
{:ok, blocker} = User.follow(blocker, blocked)
assert User.following?(blocker, blocked)
refute User.following?(blocked, blocker)
{:ok, _user_relationship} = User.block(blocker, blocked)
blocked = User.get_cached_by_id(blocked.id)
assert User.blocks?(blocker, blocked)
refute User.following?(blocker, blocked)
refute User.following?(blocked, blocker)
end
test "blocks tear down blocked->blocker follow relationships" do
blocker = insert(:user)
blocked = insert(:user)
{:ok, blocked} = User.follow(blocked, blocker)
refute User.following?(blocker, blocked)
assert User.following?(blocked, blocker)
{:ok, _user_relationship} = User.block(blocker, blocked)
blocked = User.get_cached_by_id(blocked.id)
assert User.blocks?(blocker, blocked)
refute User.following?(blocker, blocked)
refute User.following?(blocked, blocker)
end
test "blocks tear down blocked->blocker subscription relationships" do
blocker = insert(:user)
blocked = insert(:user)
{:ok, _subscription} = User.subscribe(blocked, blocker)
assert User.subscribed_to?(blocked, blocker)
refute User.subscribed_to?(blocker, blocked)
{:ok, _user_relationship} = User.block(blocker, blocked)
assert User.blocks?(blocker, blocked)
refute User.subscribed_to?(blocker, blocked)
refute User.subscribed_to?(blocked, blocker)
end
end
describe "domain blocking" do
test "blocks domains" do
user = insert(:user)
collateral_user = insert(:user, %{ap_id: "https://awful-and-rude-instance.com/user/bully"})
{:ok, user} = User.block_domain(user, "awful-and-rude-instance.com")
assert User.blocks?(user, collateral_user)
end
test "does not block domain with same end" do
user = insert(:user)
collateral_user =
insert(:user, %{ap_id: "https://another-awful-and-rude-instance.com/user/bully"})
{:ok, user} = User.block_domain(user, "awful-and-rude-instance.com")
refute User.blocks?(user, collateral_user)
end
test "does not block domain with same end if wildcard added" do
user = insert(:user)
collateral_user =
insert(:user, %{ap_id: "https://another-awful-and-rude-instance.com/user/bully"})
{:ok, user} = User.block_domain(user, "*.awful-and-rude-instance.com")
refute User.blocks?(user, collateral_user)
end
test "blocks domain with wildcard for subdomain" do
user = insert(:user)
user_from_subdomain =
insert(:user, %{ap_id: "https://subdomain.awful-and-rude-instance.com/user/bully"})
user_with_two_subdomains =
insert(:user, %{
ap_id: "https://subdomain.second_subdomain.awful-and-rude-instance.com/user/bully"
})
user_domain = insert(:user, %{ap_id: "https://awful-and-rude-instance.com/user/bully"})
{:ok, user} = User.block_domain(user, "*.awful-and-rude-instance.com")
assert User.blocks?(user, user_from_subdomain)
assert User.blocks?(user, user_with_two_subdomains)
assert User.blocks?(user, user_domain)
end
test "unblocks domains" do
user = insert(:user)
collateral_user = insert(:user, %{ap_id: "https://awful-and-rude-instance.com/user/bully"})
{:ok, user} = User.block_domain(user, "awful-and-rude-instance.com")
{:ok, user} = User.unblock_domain(user, "awful-and-rude-instance.com")
refute User.blocks?(user, collateral_user)
end
test "follows take precedence over domain blocks" do
user = insert(:user)
good_eggo = insert(:user, %{ap_id: "https://meanies.social/user/cuteposter"})
{:ok, user} = User.block_domain(user, "meanies.social")
{:ok, user} = User.follow(user, good_eggo)
refute User.blocks?(user, good_eggo)
end
end
describe "blocks_import" do
test "it imports user blocks from list" do
[user1, user2, user3] = insert_list(3, :user)
identifiers = [
user2.ap_id,
user3.nickname
]
{:ok, job} = User.blocks_import(user1, identifiers)
assert {:ok, result} = ObanHelpers.perform(job)
assert is_list(result)
assert result == [user2, user3]
end
end
describe "get_recipients_from_activity" do
test "works for announces" do
actor = insert(:user)
user = insert(:user, local: true)
{:ok, activity} = CommonAPI.post(actor, %{status: "hello"})
{:ok, announce} = CommonAPI.repeat(activity.id, user)
recipients = User.get_recipients_from_activity(announce)
assert user in recipients
end
test "get recipients" do
actor = insert(:user)
user = insert(:user, local: true)
user_two = insert(:user, local: false)
addressed = insert(:user, local: true)
addressed_remote = insert(:user, local: false)
{:ok, activity} =
CommonAPI.post(actor, %{
status: "hey @#{addressed.nickname} @#{addressed_remote.nickname}"
})
assert Enum.map([actor, addressed], & &1.ap_id) --
Enum.map(User.get_recipients_from_activity(activity), & &1.ap_id) == []
{:ok, user} = User.follow(user, actor)
{:ok, _user_two} = User.follow(user_two, actor)
recipients = User.get_recipients_from_activity(activity)
assert length(recipients) == 3
assert user in recipients
assert addressed in recipients
end
test "has following" do
actor = insert(:user)
user = insert(:user)
user_two = insert(:user)
addressed = insert(:user, local: true)
{:ok, activity} =
CommonAPI.post(actor, %{
status: "hey @#{addressed.nickname}"
})
assert Enum.map([actor, addressed], & &1.ap_id) --
Enum.map(User.get_recipients_from_activity(activity), & &1.ap_id) == []
{:ok, _actor} = User.follow(actor, user)
{:ok, _actor} = User.follow(actor, user_two)
recipients = User.get_recipients_from_activity(activity)
assert length(recipients) == 2
assert addressed in recipients
end
end
describe ".deactivate" do
test "can de-activate then re-activate a user" do
user = insert(:user)
assert false == user.deactivated
{:ok, user} = User.deactivate(user)
assert true == user.deactivated
{:ok, user} = User.deactivate(user, false)
assert false == user.deactivated
end
test "hide a user from followers" do
user = insert(:user)
user2 = insert(:user)
{:ok, user} = User.follow(user, user2)
{:ok, _user} = User.deactivate(user)
user2 = User.get_cached_by_id(user2.id)
assert user2.follower_count == 0
assert [] = User.get_followers(user2)
end
test "hide a user from friends" do
user = insert(:user)
user2 = insert(:user)
{:ok, user2} = User.follow(user2, user)
assert user2.following_count == 1
assert User.following_count(user2) == 1
{:ok, _user} = User.deactivate(user)
user2 = User.get_cached_by_id(user2.id)
assert refresh_record(user2).following_count == 0
assert user2.following_count == 0
assert User.following_count(user2) == 0
assert [] = User.get_friends(user2)
end
test "hide a user's statuses from timelines and notifications" do
user = insert(:user)
user2 = insert(:user)
{:ok, user2} = User.follow(user2, user)
{:ok, activity} = CommonAPI.post(user, %{status: "hey @#{user2.nickname}"})
activity = Repo.preload(activity, :bookmark)
[notification] = Pleroma.Notification.for_user(user2)
assert notification.activity.id == activity.id
assert [activity] == ActivityPub.fetch_public_activities(%{}) |> Repo.preload(:bookmark)
assert [%{activity | thread_muted?: CommonAPI.thread_muted?(user2, activity)}] ==
ActivityPub.fetch_activities([user2.ap_id | User.following(user2)], %{
user: user2
})
{:ok, _user} = User.deactivate(user)
assert [] == ActivityPub.fetch_public_activities(%{})
assert [] == Pleroma.Notification.for_user(user2)
assert [] ==
ActivityPub.fetch_activities([user2.ap_id | User.following(user2)], %{
user: user2
})
end
end
describe "approve" do
test "approves a user" do
user = insert(:user, approval_pending: true)
assert true == user.approval_pending
{:ok, user} = User.approve(user)
assert false == user.approval_pending
end
test "approves a list of users" do
unapproved_users = [
insert(:user, approval_pending: true),
insert(:user, approval_pending: true),
insert(:user, approval_pending: true)
]
{:ok, users} = User.approve(unapproved_users)
assert Enum.count(users) == 3
Enum.each(users, fn user ->
assert false == user.approval_pending
end)
end
end
describe "delete" do
setup do
{:ok, user} = insert(:user) |> User.set_cache()
[user: user]
end
setup do: clear_config([:instance, :federating])
test ".delete_user_activities deletes all create activities", %{user: user} do
{:ok, activity} = CommonAPI.post(user, %{status: "2hu"})
User.delete_user_activities(user)
# TODO: Test removal favorites, repeats, delete activities.
refute Activity.get_by_id(activity.id)
end
test "it deactivates a user, all follow relationships and all activities", %{user: user} do
follower = insert(:user)
{:ok, follower} = User.follow(follower, user)
locked_user = insert(:user, name: "locked", locked: true)
{:ok, _} = User.follow(user, locked_user, :follow_pending)
object = insert(:note, user: user)
activity = insert(:note_activity, user: user, note: object)
object_two = insert(:note, user: follower)
activity_two = insert(:note_activity, user: follower, note: object_two)
{:ok, like} = CommonAPI.favorite(user, activity_two.id)
{:ok, like_two} = CommonAPI.favorite(follower, activity.id)
{:ok, repeat} = CommonAPI.repeat(activity_two.id, user)
{:ok, job} = User.delete(user)
{:ok, _user} = ObanHelpers.perform(job)
follower = User.get_cached_by_id(follower.id)
refute User.following?(follower, user)
assert %{deactivated: true} = User.get_by_id(user.id)
assert [] == User.get_follow_requests(locked_user)
user_activities =
user.ap_id
|> Activity.Queries.by_actor()
|> Repo.all()
|> Enum.map(fn act -> act.data["type"] end)
assert Enum.all?(user_activities, fn act -> act in ~w(Delete Undo) end)
refute Activity.get_by_id(activity.id)
refute Activity.get_by_id(like.id)
refute Activity.get_by_id(like_two.id)
refute Activity.get_by_id(repeat.id)
end
end
describe "delete/1 when confirmation is pending" do
setup do
user = insert(:user, confirmation_pending: true)
{:ok, user: user}
end
test "deletes user from database when activation required", %{user: user} do
clear_config([:instance, :account_activation_required], true)
{:ok, job} = User.delete(user)
{:ok, _} = ObanHelpers.perform(job)
refute User.get_cached_by_id(user.id)
refute User.get_by_id(user.id)
end
test "deactivates user when activation is not required", %{user: user} do
clear_config([:instance, :account_activation_required], false)
{:ok, job} = User.delete(user)
{:ok, _} = ObanHelpers.perform(job)
assert %{deactivated: true} = User.get_cached_by_id(user.id)
assert %{deactivated: true} = User.get_by_id(user.id)
end
end
test "delete/1 when approval is pending deletes the user" do
user = insert(:user, approval_pending: true)
{:ok, job} = User.delete(user)
{:ok, _} = ObanHelpers.perform(job)
refute User.get_cached_by_id(user.id)
refute User.get_by_id(user.id)
end
test "delete/1 purges a user when they wouldn't be fully deleted" do
user =
insert(:user, %{
bio: "eyy lmao",
name: "qqqqqqq",
password_hash: "pdfk2$1b3n159001",
keys: "RSA begin buplic key",
public_key: "--PRIVATE KEYE--",
avatar: %{"a" => "b"},
tags: ["qqqqq"],
banner: %{"a" => "b"},
background: %{"a" => "b"},
note_count: 9,
follower_count: 9,
following_count: 9001,
locked: true,
confirmation_pending: true,
password_reset_pending: true,
approval_pending: true,
registration_reason: "ahhhhh",
confirmation_token: "qqqq",
domain_blocks: ["lain.com"],
deactivated: true,
ap_enabled: true,
is_moderator: true,
is_admin: true,
mastofe_settings: %{"a" => "b"},
mascot: %{"a" => "b"},
emoji: %{"a" => "b"},
pleroma_settings_store: %{"q" => "x"},
fields: [%{"gg" => "qq"}],
raw_fields: [%{"gg" => "qq"}],
discoverable: true,
also_known_as: ["https://lol.olo/users/loll"]
})
{:ok, job} = User.delete(user)
{:ok, _} = ObanHelpers.perform(job)
user = User.get_by_id(user.id)
assert %User{
bio: "",
raw_bio: nil,
email: nil,
name: nil,
password_hash: nil,
keys: nil,
public_key: nil,
avatar: %{},
tags: [],
last_refreshed_at: nil,
last_digest_emailed_at: nil,
banner: %{},
background: %{},
note_count: 0,
follower_count: 0,
following_count: 0,
locked: false,
confirmation_pending: false,
password_reset_pending: false,
approval_pending: false,
registration_reason: nil,
confirmation_token: nil,
domain_blocks: [],
deactivated: true,
ap_enabled: false,
is_moderator: false,
is_admin: false,
mastofe_settings: nil,
mascot: nil,
emoji: %{},
pleroma_settings_store: %{},
fields: [],
raw_fields: [],
discoverable: false,
also_known_as: []
} = user
end
test "get_public_key_for_ap_id fetches a user that's not in the db" do
assert {:ok, _key} = User.get_public_key_for_ap_id("http://mastodon.example.org/users/admin")
end
describe "per-user rich-text filtering" do
test "html_filter_policy returns default policies, when rich-text is enabled" do
user = insert(:user)
assert Pleroma.Config.get([:markup, :scrub_policy]) == User.html_filter_policy(user)
end
test "html_filter_policy returns TwitterText scrubber when rich-text is disabled" do
user = insert(:user, no_rich_text: true)
assert Pleroma.HTML.Scrubber.TwitterText == User.html_filter_policy(user)
end
end
describe "caching" do
test "invalidate_cache works" do
user = insert(:user)
User.set_cache(user)
User.invalidate_cache(user)
{:ok, nil} = Cachex.get(:user_cache, "ap_id:#{user.ap_id}")
{:ok, nil} = Cachex.get(:user_cache, "nickname:#{user.nickname}")
end
test "User.delete() plugs any possible zombie objects" do
user = insert(:user)
{:ok, job} = User.delete(user)
{:ok, _} = ObanHelpers.perform(job)
{:ok, cached_user} = Cachex.get(:user_cache, "ap_id:#{user.ap_id}")
assert cached_user != user
{:ok, cached_user} = Cachex.get(:user_cache, "nickname:#{user.ap_id}")
assert cached_user != user
end
end
describe "account_status/1" do
setup do: clear_config([:instance, :account_activation_required])
test "return confirmation_pending for unconfirm user" do
Pleroma.Config.put([:instance, :account_activation_required], true)
user = insert(:user, confirmation_pending: true)
assert User.account_status(user) == :confirmation_pending
end
test "return active for confirmed user" do
Pleroma.Config.put([:instance, :account_activation_required], true)
user = insert(:user, confirmation_pending: false)
assert User.account_status(user) == :active
end
test "return active for remote user" do
user = insert(:user, local: false)
assert User.account_status(user) == :active
end
test "returns :password_reset_pending for user with reset password" do
user = insert(:user, password_reset_pending: true)
assert User.account_status(user) == :password_reset_pending
end
test "returns :deactivated for deactivated user" do
user = insert(:user, local: true, confirmation_pending: false, deactivated: true)
assert User.account_status(user) == :deactivated
end
test "returns :approval_pending for unapproved user" do
user = insert(:user, local: true, approval_pending: true)
assert User.account_status(user) == :approval_pending
user = insert(:user, local: true, confirmation_pending: true, approval_pending: true)
assert User.account_status(user) == :approval_pending
end
end
describe "superuser?/1" do
test "returns false for unprivileged users" do
user = insert(:user, local: true)
refute User.superuser?(user)
end
test "returns false for remote users" do
user = insert(:user, local: false)
remote_admin_user = insert(:user, local: false, is_admin: true)
refute User.superuser?(user)
refute User.superuser?(remote_admin_user)
end
test "returns true for local moderators" do
user = insert(:user, local: true, is_moderator: true)
assert User.superuser?(user)
end
test "returns true for local admins" do
user = insert(:user, local: true, is_admin: true)
assert User.superuser?(user)
end
end
describe "invisible?/1" do
test "returns true for an invisible user" do
user = insert(:user, local: true, invisible: true)
assert User.invisible?(user)
end
test "returns false for a non-invisible user" do
user = insert(:user, local: true)
refute User.invisible?(user)
end
end
describe "visible_for/2" do
test "returns true when the account is itself" do
user = insert(:user, local: true)
assert User.visible_for(user, user) == :visible
end
test "returns false when the account is unauthenticated and auth is required" do
Pleroma.Config.put([:instance, :account_activation_required], true)
user = insert(:user, local: true, confirmation_pending: true)
other_user = insert(:user, local: true)
refute User.visible_for(user, other_user) == :visible
end
test "returns true when the account is unauthenticated and auth is not required" do
user = insert(:user, local: true, confirmation_pending: true)
other_user = insert(:user, local: true)
assert User.visible_for(user, other_user) == :visible
end
test "returns true when the account is unauthenticated and being viewed by a privileged account (auth required)" do
Pleroma.Config.put([:instance, :account_activation_required], true)
user = insert(:user, local: true, confirmation_pending: true)
other_user = insert(:user, local: true, is_admin: true)
assert User.visible_for(user, other_user) == :visible
end
end
describe "parse_bio/2" do
test "preserves hosts in user links text" do
remote_user = insert(:user, local: false, nickname: "nick@domain.com")
user = insert(:user)
bio = "A.k.a. @nick@domain.com"
expected_text =
~s(A.k.a. <span class="h-card"><a class="u-url mention" data-user="#{remote_user.id}" href="#{
remote_user.ap_id
}" rel="ugc">@<span>nick@domain.com</span></a></span>)
assert expected_text == User.parse_bio(bio, user)
end
test "Adds rel=me on linkbacked urls" do
user = insert(:user, ap_id: "https://social.example.org/users/lain")
bio = "http://example.com/rel_me/null"
expected_text = "<a href=\"#{bio}\">#{bio}</a>"
assert expected_text == User.parse_bio(bio, user)
bio = "http://example.com/rel_me/link"
expected_text = "<a href=\"#{bio}\" rel=\"me\">#{bio}</a>"
assert expected_text == User.parse_bio(bio, user)
bio = "http://example.com/rel_me/anchor"
expected_text = "<a href=\"#{bio}\" rel=\"me\">#{bio}</a>"
assert expected_text == User.parse_bio(bio, user)
end
end
test "follower count is updated when a follower is blocked" do
user = insert(:user)
follower = insert(:user)
follower2 = insert(:user)
follower3 = insert(:user)
{:ok, follower} = User.follow(follower, user)
{:ok, _follower2} = User.follow(follower2, user)
{:ok, _follower3} = User.follow(follower3, user)
{:ok, _user_relationship} = User.block(user, follower)
user = refresh_record(user)
assert user.follower_count == 2
end
describe "list_inactive_users_query/1" do
defp days_ago(days) do
NaiveDateTime.add(
NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second),
-days * 60 * 60 * 24,
:second
)
end
test "Users are inactive by default" do
total = 10
users =
Enum.map(1..total, fn _ ->
insert(:user, last_digest_emailed_at: days_ago(20), deactivated: false)
end)
inactive_users_ids =
Pleroma.User.list_inactive_users_query()
|> Pleroma.Repo.all()
|> Enum.map(& &1.id)
Enum.each(users, fn user ->
assert user.id in inactive_users_ids
end)
end
test "Only includes users who has no recent activity" do
total = 10
users =
Enum.map(1..total, fn _ ->
insert(:user, last_digest_emailed_at: days_ago(20), deactivated: false)
end)
{inactive, active} = Enum.split(users, trunc(total / 2))
Enum.map(active, fn user ->
to = Enum.random(users -- [user])
{:ok, _} =
CommonAPI.post(user, %{
status: "hey @#{to.nickname}"
})
end)
inactive_users_ids =
Pleroma.User.list_inactive_users_query()
|> Pleroma.Repo.all()
|> Enum.map(& &1.id)
Enum.each(active, fn user ->
refute user.id in inactive_users_ids
end)
Enum.each(inactive, fn user ->
assert user.id in inactive_users_ids
end)
end
test "Only includes users with no read notifications" do
total = 10
users =
Enum.map(1..total, fn _ ->
insert(:user, last_digest_emailed_at: days_ago(20), deactivated: false)
end)
[sender | recipients] = users
{inactive, active} = Enum.split(recipients, trunc(total / 2))
Enum.each(recipients, fn to ->
{:ok, _} =
CommonAPI.post(sender, %{
status: "hey @#{to.nickname}"
})
{:ok, _} =
CommonAPI.post(sender, %{
status: "hey again @#{to.nickname}"
})
end)
Enum.each(active, fn user ->
[n1, _n2] = Pleroma.Notification.for_user(user)
{:ok, _} = Pleroma.Notification.read_one(user, n1.id)
end)
inactive_users_ids =
Pleroma.User.list_inactive_users_query()
|> Pleroma.Repo.all()
|> Enum.map(& &1.id)
Enum.each(active, fn user ->
refute user.id in inactive_users_ids
end)
Enum.each(inactive, fn user ->
assert user.id in inactive_users_ids
end)
end
end
describe "toggle_confirmation/1" do
test "if user is confirmed" do
user = insert(:user, confirmation_pending: false)
{:ok, user} = User.toggle_confirmation(user)
assert user.confirmation_pending
assert user.confirmation_token
end
test "if user is unconfirmed" do
user = insert(:user, confirmation_pending: true, confirmation_token: "some token")
{:ok, user} = User.toggle_confirmation(user)
refute user.confirmation_pending
refute user.confirmation_token
end
end
describe "ensure_keys_present" do
test "it creates keys for a user and stores them in info" do
user = insert(:user)
refute is_binary(user.keys)
{:ok, user} = User.ensure_keys_present(user)
assert is_binary(user.keys)
end
test "it doesn't create keys if there already are some" do
user = insert(:user, keys: "xxx")
{:ok, user} = User.ensure_keys_present(user)
assert user.keys == "xxx"
end
end
describe "get_ap_ids_by_nicknames" do
test "it returns a list of AP ids for a given set of nicknames" do
user = insert(:user)
user_two = insert(:user)
ap_ids = User.get_ap_ids_by_nicknames([user.nickname, user_two.nickname, "nonexistent"])
assert length(ap_ids) == 2
assert user.ap_id in ap_ids
assert user_two.ap_id in ap_ids
end
end
describe "sync followers count" do
setup do
user1 = insert(:user, local: false, ap_id: "http://localhost:4001/users/masto_closed")
user2 = insert(:user, local: false, ap_id: "http://localhost:4001/users/fuser2")
insert(:user, local: true)
insert(:user, local: false, deactivated: true)
{:ok, user1: user1, user2: user2}
end
test "external_users/1 external active users with limit", %{user1: user1, user2: user2} do
[fdb_user1] = User.external_users(limit: 1)
assert fdb_user1.ap_id
assert fdb_user1.ap_id == user1.ap_id
assert fdb_user1.id == user1.id
[fdb_user2] = User.external_users(max_id: fdb_user1.id, limit: 1)
assert fdb_user2.ap_id
assert fdb_user2.ap_id == user2.ap_id
assert fdb_user2.id == user2.id
assert User.external_users(max_id: fdb_user2.id, limit: 1) == []
end
end
describe "is_internal_user?/1" do
test "non-internal user returns false" do
user = insert(:user)
refute User.is_internal_user?(user)
end
test "user with no nickname returns true" do
user = insert(:user, %{nickname: nil})
assert User.is_internal_user?(user)
end
test "user with internal-prefixed nickname returns true" do
user = insert(:user, %{nickname: "internal.test"})
assert User.is_internal_user?(user)
end
end
describe "update_and_set_cache/1" do
test "returns error when user is stale instead Ecto.StaleEntryError" do
user = insert(:user)
changeset = Ecto.Changeset.change(user, bio: "test")
Repo.delete(user)
assert {:error, %Ecto.Changeset{errors: [id: {"is stale", [stale: true]}], valid?: false}} =
User.update_and_set_cache(changeset)
end
test "performs update cache if user updated" do
user = insert(:user)
assert {:ok, nil} = Cachex.get(:user_cache, "ap_id:#{user.ap_id}")
changeset = Ecto.Changeset.change(user, bio: "test-bio")
assert {:ok, %User{bio: "test-bio"} = user} = User.update_and_set_cache(changeset)
assert {:ok, user} = Cachex.get(:user_cache, "ap_id:#{user.ap_id}")
assert %User{bio: "test-bio"} = User.get_cached_by_ap_id(user.ap_id)
end
end
describe "following/followers synchronization" do
setup do: clear_config([:instance, :external_user_synchronization])
test "updates the counters normally on following/getting a follow when disabled" do
Pleroma.Config.put([:instance, :external_user_synchronization], false)
user = insert(:user)
other_user =
insert(:user,
local: false,
follower_address: "http://localhost:4001/users/masto_closed/followers",
following_address: "http://localhost:4001/users/masto_closed/following",
ap_enabled: true
)
assert other_user.following_count == 0
assert other_user.follower_count == 0
{:ok, user} = Pleroma.User.follow(user, other_user)
other_user = Pleroma.User.get_by_id(other_user.id)
assert user.following_count == 1
assert other_user.follower_count == 1
end
test "syncronizes the counters with the remote instance for the followed when enabled" do
Pleroma.Config.put([:instance, :external_user_synchronization], false)
user = insert(:user)
other_user =
insert(:user,
local: false,
follower_address: "http://localhost:4001/users/masto_closed/followers",
following_address: "http://localhost:4001/users/masto_closed/following",
ap_enabled: true
)
assert other_user.following_count == 0
assert other_user.follower_count == 0
Pleroma.Config.put([:instance, :external_user_synchronization], true)
{:ok, _user} = User.follow(user, other_user)
other_user = User.get_by_id(other_user.id)
assert other_user.follower_count == 437
end
test "syncronizes the counters with the remote instance for the follower when enabled" do
Pleroma.Config.put([:instance, :external_user_synchronization], false)
user = insert(:user)
other_user =
insert(:user,
local: false,
follower_address: "http://localhost:4001/users/masto_closed/followers",
following_address: "http://localhost:4001/users/masto_closed/following",
ap_enabled: true
)
assert other_user.following_count == 0
assert other_user.follower_count == 0
Pleroma.Config.put([:instance, :external_user_synchronization], true)
{:ok, other_user} = User.follow(other_user, user)
assert other_user.following_count == 152
end
end
describe "change_email/2" do
setup do
[user: insert(:user)]
end
test "blank email returns error", %{user: user} do
assert {:error, %{errors: [email: {"can't be blank", _}]}} = User.change_email(user, "")
assert {:error, %{errors: [email: {"can't be blank", _}]}} = User.change_email(user, nil)
end
test "non unique email returns error", %{user: user} do
%{email: email} = insert(:user)
assert {:error, %{errors: [email: {"has already been taken", _}]}} =
User.change_email(user, email)
end
test "invalid email returns error", %{user: user} do
assert {:error, %{errors: [email: {"has invalid format", _}]}} =
User.change_email(user, "cofe")
end
test "changes email", %{user: user} do
assert {:ok, %User{email: "cofe@cofe.party"}} = User.change_email(user, "cofe@cofe.party")
end
end
describe "get_cached_by_nickname_or_id" do
setup do
local_user = insert(:user)
remote_user = insert(:user, nickname: "nickname@example.com", local: false)
[local_user: local_user, remote_user: remote_user]
end
setup do: clear_config([:instance, :limit_to_local_content])
test "allows getting remote users by id no matter what :limit_to_local_content is set to", %{
remote_user: remote_user
} do
Pleroma.Config.put([:instance, :limit_to_local_content], false)
assert %User{} = User.get_cached_by_nickname_or_id(remote_user.id)
Pleroma.Config.put([:instance, :limit_to_local_content], true)
assert %User{} = User.get_cached_by_nickname_or_id(remote_user.id)
Pleroma.Config.put([:instance, :limit_to_local_content], :unauthenticated)
assert %User{} = User.get_cached_by_nickname_or_id(remote_user.id)
end
test "disallows getting remote users by nickname without authentication when :limit_to_local_content is set to :unauthenticated",
%{remote_user: remote_user} do
Pleroma.Config.put([:instance, :limit_to_local_content], :unauthenticated)
assert nil == User.get_cached_by_nickname_or_id(remote_user.nickname)
end
test "allows getting remote users by nickname with authentication when :limit_to_local_content is set to :unauthenticated",
%{remote_user: remote_user, local_user: local_user} do
Pleroma.Config.put([:instance, :limit_to_local_content], :unauthenticated)
assert %User{} = User.get_cached_by_nickname_or_id(remote_user.nickname, for: local_user)
end
test "disallows getting remote users by nickname when :limit_to_local_content is set to true",
%{remote_user: remote_user} do
Pleroma.Config.put([:instance, :limit_to_local_content], true)
assert nil == User.get_cached_by_nickname_or_id(remote_user.nickname)
end
test "allows getting local users by nickname no matter what :limit_to_local_content is set to",
%{local_user: local_user} do
Pleroma.Config.put([:instance, :limit_to_local_content], false)
assert %User{} = User.get_cached_by_nickname_or_id(local_user.nickname)
Pleroma.Config.put([:instance, :limit_to_local_content], true)
assert %User{} = User.get_cached_by_nickname_or_id(local_user.nickname)
Pleroma.Config.put([:instance, :limit_to_local_content], :unauthenticated)
assert %User{} = User.get_cached_by_nickname_or_id(local_user.nickname)
end
end
describe "update_email_notifications/2" do
setup do
user = insert(:user, email_notifications: %{"digest" => true})
{:ok, user: user}
end
test "Notifications are updated", %{user: user} do
true = user.email_notifications["digest"]
assert {:ok, result} = User.update_email_notifications(user, %{"digest" => false})
assert result.email_notifications["digest"] == false
end
end
test "avatar fallback" do
user = insert(:user)
assert User.avatar_url(user) =~ "/images/avi.png"
clear_config([:assets, :default_user_avatar], "avatar.png")
user = User.get_cached_by_nickname_or_id(user.nickname)
assert User.avatar_url(user) =~ "avatar.png"
assert User.avatar_url(user, no_default: true) == nil
end
end
diff --git a/test/web/mastodon_api/controllers/notification_controller_test.exs b/test/web/mastodon_api/controllers/notification_controller_test.exs
index 70ef0e8b5..5fd518c60 100644
--- a/test/web/mastodon_api/controllers/notification_controller_test.exs
+++ b/test/web/mastodon_api/controllers/notification_controller_test.exs
@@ -1,626 +1,626 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.NotificationControllerTest do
use Pleroma.Web.ConnCase
alias Pleroma.Notification
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.CommonAPI
import Pleroma.Factory
test "does NOT render account/pleroma/relationship by default" do
%{user: user, conn: conn} = oauth_access(["read:notifications"])
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"})
{:ok, [_notification]} = Notification.create_notifications(activity)
response =
conn
|> assign(:user, user)
|> get("/api/v1/notifications")
|> json_response_and_validate_schema(200)
assert Enum.all?(response, fn n ->
get_in(n, ["account", "pleroma", "relationship"]) == %{}
end)
end
test "list of notifications" do
%{user: user, conn: conn} = oauth_access(["read:notifications"])
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"})
{:ok, [_notification]} = Notification.create_notifications(activity)
conn =
conn
|> assign(:user, user)
|> get("/api/v1/notifications")
expected_response =
"hi <span class=\"h-card\"><a class=\"u-url mention\" data-user=\"#{user.id}\" href=\"#{
user.ap_id
}\" rel=\"ugc\">@<span>#{user.nickname}</span></a></span>"
assert [%{"status" => %{"content" => response}} | _rest] =
json_response_and_validate_schema(conn, 200)
assert response == expected_response
end
test "by default, does not contain pleroma:chat_mention" do
%{user: user, conn: conn} = oauth_access(["read:notifications"])
other_user = insert(:user)
{:ok, _activity} = CommonAPI.post_chat_message(other_user, user, "hey")
result =
conn
|> get("/api/v1/notifications")
|> json_response_and_validate_schema(200)
assert [] == result
result =
conn
|> get("/api/v1/notifications?include_types[]=pleroma:chat_mention")
|> json_response_and_validate_schema(200)
assert [_] = result
end
test "getting a single notification" do
%{user: user, conn: conn} = oauth_access(["read:notifications"])
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"})
{:ok, [notification]} = Notification.create_notifications(activity)
conn = get(conn, "/api/v1/notifications/#{notification.id}")
expected_response =
"hi <span class=\"h-card\"><a class=\"u-url mention\" data-user=\"#{user.id}\" href=\"#{
user.ap_id
}\" rel=\"ugc\">@<span>#{user.nickname}</span></a></span>"
assert %{"status" => %{"content" => response}} = json_response_and_validate_schema(conn, 200)
assert response == expected_response
end
test "dismissing a single notification (deprecated endpoint)" do
%{user: user, conn: conn} = oauth_access(["write:notifications"])
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"})
{:ok, [notification]} = Notification.create_notifications(activity)
conn =
conn
|> assign(:user, user)
|> put_req_header("content-type", "application/json")
|> post("/api/v1/notifications/dismiss", %{"id" => to_string(notification.id)})
assert %{} = json_response_and_validate_schema(conn, 200)
end
test "dismissing a single notification" do
%{user: user, conn: conn} = oauth_access(["write:notifications"])
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"})
{:ok, [notification]} = Notification.create_notifications(activity)
conn =
conn
|> assign(:user, user)
|> post("/api/v1/notifications/#{notification.id}/dismiss")
assert %{} = json_response_and_validate_schema(conn, 200)
end
test "clearing all notifications" do
%{user: user, conn: conn} = oauth_access(["write:notifications", "read:notifications"])
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"})
{:ok, [_notification]} = Notification.create_notifications(activity)
ret_conn = post(conn, "/api/v1/notifications/clear")
assert %{} = json_response_and_validate_schema(ret_conn, 200)
ret_conn = get(conn, "/api/v1/notifications")
assert all = json_response_and_validate_schema(ret_conn, 200)
assert all == []
end
test "paginates notifications using min_id, since_id, max_id, and limit" do
%{user: user, conn: conn} = oauth_access(["read:notifications"])
other_user = insert(:user)
{:ok, activity1} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"})
{:ok, activity2} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"})
{:ok, activity3} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"})
{:ok, activity4} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"})
notification1_id = get_notification_id_by_activity(activity1)
notification2_id = get_notification_id_by_activity(activity2)
notification3_id = get_notification_id_by_activity(activity3)
notification4_id = get_notification_id_by_activity(activity4)
conn = assign(conn, :user, user)
# min_id
result =
conn
|> get("/api/v1/notifications?limit=2&min_id=#{notification1_id}")
|> json_response_and_validate_schema(:ok)
assert [%{"id" => ^notification3_id}, %{"id" => ^notification2_id}] = result
# since_id
result =
conn
|> get("/api/v1/notifications?limit=2&since_id=#{notification1_id}")
|> json_response_and_validate_schema(:ok)
assert [%{"id" => ^notification4_id}, %{"id" => ^notification3_id}] = result
# max_id
result =
conn
|> get("/api/v1/notifications?limit=2&max_id=#{notification4_id}")
|> json_response_and_validate_schema(:ok)
assert [%{"id" => ^notification3_id}, %{"id" => ^notification2_id}] = result
end
describe "exclude_visibilities" do
test "filters notifications for mentions" do
%{user: user, conn: conn} = oauth_access(["read:notifications"])
other_user = insert(:user)
{:ok, public_activity} =
CommonAPI.post(other_user, %{status: "@#{user.nickname}", visibility: "public"})
{:ok, direct_activity} =
CommonAPI.post(other_user, %{status: "@#{user.nickname}", visibility: "direct"})
{:ok, unlisted_activity} =
CommonAPI.post(other_user, %{status: "@#{user.nickname}", visibility: "unlisted"})
{:ok, private_activity} =
CommonAPI.post(other_user, %{status: "@#{user.nickname}", visibility: "private"})
query = params_to_query(%{exclude_visibilities: ["public", "unlisted", "private"]})
conn_res = get(conn, "/api/v1/notifications?" <> query)
assert [%{"status" => %{"id" => id}}] = json_response_and_validate_schema(conn_res, 200)
assert id == direct_activity.id
query = params_to_query(%{exclude_visibilities: ["public", "unlisted", "direct"]})
conn_res = get(conn, "/api/v1/notifications?" <> query)
assert [%{"status" => %{"id" => id}}] = json_response_and_validate_schema(conn_res, 200)
assert id == private_activity.id
query = params_to_query(%{exclude_visibilities: ["public", "private", "direct"]})
conn_res = get(conn, "/api/v1/notifications?" <> query)
assert [%{"status" => %{"id" => id}}] = json_response_and_validate_schema(conn_res, 200)
assert id == unlisted_activity.id
query = params_to_query(%{exclude_visibilities: ["unlisted", "private", "direct"]})
conn_res = get(conn, "/api/v1/notifications?" <> query)
assert [%{"status" => %{"id" => id}}] = json_response_and_validate_schema(conn_res, 200)
assert id == public_activity.id
end
test "filters notifications for Like activities" do
user = insert(:user)
%{user: other_user, conn: conn} = oauth_access(["read:notifications"])
{:ok, public_activity} = CommonAPI.post(other_user, %{status: ".", visibility: "public"})
{:ok, direct_activity} =
CommonAPI.post(other_user, %{status: "@#{user.nickname}", visibility: "direct"})
{:ok, unlisted_activity} =
CommonAPI.post(other_user, %{status: ".", visibility: "unlisted"})
{:ok, private_activity} = CommonAPI.post(other_user, %{status: ".", visibility: "private"})
{:ok, _} = CommonAPI.favorite(user, public_activity.id)
{:ok, _} = CommonAPI.favorite(user, direct_activity.id)
{:ok, _} = CommonAPI.favorite(user, unlisted_activity.id)
{:ok, _} = CommonAPI.favorite(user, private_activity.id)
activity_ids =
conn
|> get("/api/v1/notifications?exclude_visibilities[]=direct")
|> json_response_and_validate_schema(200)
|> Enum.map(& &1["status"]["id"])
assert public_activity.id in activity_ids
assert unlisted_activity.id in activity_ids
assert private_activity.id in activity_ids
refute direct_activity.id in activity_ids
activity_ids =
conn
|> get("/api/v1/notifications?exclude_visibilities[]=unlisted")
|> json_response_and_validate_schema(200)
|> Enum.map(& &1["status"]["id"])
assert public_activity.id in activity_ids
refute unlisted_activity.id in activity_ids
assert private_activity.id in activity_ids
assert direct_activity.id in activity_ids
activity_ids =
conn
|> get("/api/v1/notifications?exclude_visibilities[]=private")
|> json_response_and_validate_schema(200)
|> Enum.map(& &1["status"]["id"])
assert public_activity.id in activity_ids
assert unlisted_activity.id in activity_ids
refute private_activity.id in activity_ids
assert direct_activity.id in activity_ids
activity_ids =
conn
|> get("/api/v1/notifications?exclude_visibilities[]=public")
|> json_response_and_validate_schema(200)
|> Enum.map(& &1["status"]["id"])
refute public_activity.id in activity_ids
assert unlisted_activity.id in activity_ids
assert private_activity.id in activity_ids
assert direct_activity.id in activity_ids
end
test "filters notifications for Announce activities" do
user = insert(:user)
%{user: other_user, conn: conn} = oauth_access(["read:notifications"])
{:ok, public_activity} = CommonAPI.post(other_user, %{status: ".", visibility: "public"})
{:ok, unlisted_activity} =
CommonAPI.post(other_user, %{status: ".", visibility: "unlisted"})
{:ok, _} = CommonAPI.repeat(public_activity.id, user)
{:ok, _} = CommonAPI.repeat(unlisted_activity.id, user)
activity_ids =
conn
|> get("/api/v1/notifications?exclude_visibilities[]=unlisted")
|> json_response_and_validate_schema(200)
|> Enum.map(& &1["status"]["id"])
assert public_activity.id in activity_ids
refute unlisted_activity.id in activity_ids
end
test "doesn't return less than the requested amount of records when the user's reply is liked" do
user = insert(:user)
%{user: other_user, conn: conn} = oauth_access(["read:notifications"])
{:ok, mention} =
CommonAPI.post(user, %{status: "@#{other_user.nickname}", visibility: "public"})
{:ok, activity} = CommonAPI.post(user, %{status: ".", visibility: "public"})
{:ok, reply} =
CommonAPI.post(other_user, %{
status: ".",
visibility: "public",
in_reply_to_status_id: activity.id
})
{:ok, _favorite} = CommonAPI.favorite(user, reply.id)
activity_ids =
conn
|> get("/api/v1/notifications?exclude_visibilities[]=direct&limit=2")
|> json_response_and_validate_schema(200)
|> Enum.map(& &1["status"]["id"])
assert [reply.id, mention.id] == activity_ids
end
end
test "filters notifications using exclude_types" do
%{user: user, conn: conn} = oauth_access(["read:notifications"])
other_user = insert(:user)
{:ok, mention_activity} = CommonAPI.post(other_user, %{status: "hey @#{user.nickname}"})
{:ok, create_activity} = CommonAPI.post(user, %{status: "hey"})
{:ok, favorite_activity} = CommonAPI.favorite(other_user, create_activity.id)
{:ok, reblog_activity} = CommonAPI.repeat(create_activity.id, other_user)
{:ok, _, _, follow_activity} = CommonAPI.follow(other_user, user)
mention_notification_id = get_notification_id_by_activity(mention_activity)
favorite_notification_id = get_notification_id_by_activity(favorite_activity)
reblog_notification_id = get_notification_id_by_activity(reblog_activity)
follow_notification_id = get_notification_id_by_activity(follow_activity)
query = params_to_query(%{exclude_types: ["mention", "favourite", "reblog"]})
conn_res = get(conn, "/api/v1/notifications?" <> query)
assert [%{"id" => ^follow_notification_id}] = json_response_and_validate_schema(conn_res, 200)
query = params_to_query(%{exclude_types: ["favourite", "reblog", "follow"]})
conn_res = get(conn, "/api/v1/notifications?" <> query)
assert [%{"id" => ^mention_notification_id}] =
json_response_and_validate_schema(conn_res, 200)
query = params_to_query(%{exclude_types: ["reblog", "follow", "mention"]})
conn_res = get(conn, "/api/v1/notifications?" <> query)
assert [%{"id" => ^favorite_notification_id}] =
json_response_and_validate_schema(conn_res, 200)
query = params_to_query(%{exclude_types: ["follow", "mention", "favourite"]})
conn_res = get(conn, "/api/v1/notifications?" <> query)
assert [%{"id" => ^reblog_notification_id}] = json_response_and_validate_schema(conn_res, 200)
end
test "filters notifications using include_types" do
%{user: user, conn: conn} = oauth_access(["read:notifications"])
other_user = insert(:user)
{:ok, mention_activity} = CommonAPI.post(other_user, %{status: "hey @#{user.nickname}"})
{:ok, create_activity} = CommonAPI.post(user, %{status: "hey"})
{:ok, favorite_activity} = CommonAPI.favorite(other_user, create_activity.id)
{:ok, reblog_activity} = CommonAPI.repeat(create_activity.id, other_user)
{:ok, _, _, follow_activity} = CommonAPI.follow(other_user, user)
mention_notification_id = get_notification_id_by_activity(mention_activity)
favorite_notification_id = get_notification_id_by_activity(favorite_activity)
reblog_notification_id = get_notification_id_by_activity(reblog_activity)
follow_notification_id = get_notification_id_by_activity(follow_activity)
conn_res = get(conn, "/api/v1/notifications?include_types[]=follow")
assert [%{"id" => ^follow_notification_id}] = json_response_and_validate_schema(conn_res, 200)
conn_res = get(conn, "/api/v1/notifications?include_types[]=mention")
assert [%{"id" => ^mention_notification_id}] =
json_response_and_validate_schema(conn_res, 200)
conn_res = get(conn, "/api/v1/notifications?include_types[]=favourite")
assert [%{"id" => ^favorite_notification_id}] =
json_response_and_validate_schema(conn_res, 200)
conn_res = get(conn, "/api/v1/notifications?include_types[]=reblog")
assert [%{"id" => ^reblog_notification_id}] = json_response_and_validate_schema(conn_res, 200)
result = conn |> get("/api/v1/notifications") |> json_response_and_validate_schema(200)
assert length(result) == 4
query = params_to_query(%{include_types: ["follow", "mention", "favourite", "reblog"]})
result =
conn
|> get("/api/v1/notifications?" <> query)
|> json_response_and_validate_schema(200)
assert length(result) == 4
end
test "destroy multiple" do
%{user: user, conn: conn} = oauth_access(["read:notifications", "write:notifications"])
other_user = insert(:user)
{:ok, activity1} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"})
{:ok, activity2} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"})
{:ok, activity3} = CommonAPI.post(user, %{status: "hi @#{other_user.nickname}"})
{:ok, activity4} = CommonAPI.post(user, %{status: "hi @#{other_user.nickname}"})
notification1_id = get_notification_id_by_activity(activity1)
notification2_id = get_notification_id_by_activity(activity2)
notification3_id = get_notification_id_by_activity(activity3)
notification4_id = get_notification_id_by_activity(activity4)
result =
conn
|> get("/api/v1/notifications")
|> json_response_and_validate_schema(:ok)
assert [%{"id" => ^notification2_id}, %{"id" => ^notification1_id}] = result
conn2 =
conn
|> assign(:user, other_user)
|> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:notifications"]))
result =
conn2
|> get("/api/v1/notifications")
|> json_response_and_validate_schema(:ok)
assert [%{"id" => ^notification4_id}, %{"id" => ^notification3_id}] = result
query = params_to_query(%{ids: [notification1_id, notification2_id]})
conn_destroy = delete(conn, "/api/v1/notifications/destroy_multiple?" <> query)
assert json_response_and_validate_schema(conn_destroy, 200) == %{}
result =
conn2
|> get("/api/v1/notifications")
|> json_response_and_validate_schema(:ok)
assert [%{"id" => ^notification4_id}, %{"id" => ^notification3_id}] = result
end
test "doesn't see notifications after muting user with notifications" do
%{user: user, conn: conn} = oauth_access(["read:notifications"])
user2 = insert(:user)
{:ok, _, _, _} = CommonAPI.follow(user, user2)
{:ok, _} = CommonAPI.post(user2, %{status: "hey @#{user.nickname}"})
ret_conn = get(conn, "/api/v1/notifications")
assert length(json_response_and_validate_schema(ret_conn, 200)) == 1
{:ok, _user_relationships} = User.mute(user, user2)
conn = get(conn, "/api/v1/notifications")
assert json_response_and_validate_schema(conn, 200) == []
end
test "see notifications after muting user without notifications" do
%{user: user, conn: conn} = oauth_access(["read:notifications"])
user2 = insert(:user)
{:ok, _, _, _} = CommonAPI.follow(user, user2)
{:ok, _} = CommonAPI.post(user2, %{status: "hey @#{user.nickname}"})
ret_conn = get(conn, "/api/v1/notifications")
assert length(json_response_and_validate_schema(ret_conn, 200)) == 1
- {:ok, _user_relationships} = User.mute(user, user2, false)
+ {:ok, _user_relationships} = User.mute(user, user2, %{notifications: false})
conn = get(conn, "/api/v1/notifications")
assert length(json_response_and_validate_schema(conn, 200)) == 1
end
test "see notifications after muting user with notifications and with_muted parameter" do
%{user: user, conn: conn} = oauth_access(["read:notifications"])
user2 = insert(:user)
{:ok, _, _, _} = CommonAPI.follow(user, user2)
{:ok, _} = CommonAPI.post(user2, %{status: "hey @#{user.nickname}"})
ret_conn = get(conn, "/api/v1/notifications")
assert length(json_response_and_validate_schema(ret_conn, 200)) == 1
{:ok, _user_relationships} = User.mute(user, user2)
conn = get(conn, "/api/v1/notifications?with_muted=true")
assert length(json_response_and_validate_schema(conn, 200)) == 1
end
@tag capture_log: true
test "see move notifications" do
old_user = insert(:user)
new_user = insert(:user, also_known_as: [old_user.ap_id])
%{user: follower, conn: conn} = oauth_access(["read:notifications"])
old_user_url = old_user.ap_id
body =
File.read!("test/fixtures/users_mock/localhost.json")
|> String.replace("{{nickname}}", old_user.nickname)
|> Jason.encode!()
Tesla.Mock.mock(fn
%{method: :get, url: ^old_user_url} ->
%Tesla.Env{status: 200, body: body}
end)
User.follow(follower, old_user)
Pleroma.Web.ActivityPub.ActivityPub.move(old_user, new_user)
Pleroma.Tests.ObanHelpers.perform_all()
conn = get(conn, "/api/v1/notifications")
assert length(json_response_and_validate_schema(conn, 200)) == 1
end
describe "link headers" do
test "preserves parameters in link headers" do
%{user: user, conn: conn} = oauth_access(["read:notifications"])
other_user = insert(:user)
{:ok, activity1} =
CommonAPI.post(other_user, %{
status: "hi @#{user.nickname}",
visibility: "public"
})
{:ok, activity2} =
CommonAPI.post(other_user, %{
status: "hi @#{user.nickname}",
visibility: "public"
})
notification1 = Repo.get_by(Notification, activity_id: activity1.id)
notification2 = Repo.get_by(Notification, activity_id: activity2.id)
conn =
conn
|> assign(:user, user)
|> get("/api/v1/notifications?limit=5")
assert [link_header] = get_resp_header(conn, "link")
assert link_header =~ ~r/limit=5/
assert link_header =~ ~r/min_id=#{notification2.id}/
assert link_header =~ ~r/max_id=#{notification1.id}/
end
end
describe "from specified user" do
test "account_id" do
%{user: user, conn: conn} = oauth_access(["read:notifications"])
%{id: account_id} = other_user1 = insert(:user)
other_user2 = insert(:user)
{:ok, _activity} = CommonAPI.post(other_user1, %{status: "hi @#{user.nickname}"})
{:ok, _activity} = CommonAPI.post(other_user2, %{status: "bye @#{user.nickname}"})
assert [%{"account" => %{"id" => ^account_id}}] =
conn
|> assign(:user, user)
|> get("/api/v1/notifications?account_id=#{account_id}")
|> json_response_and_validate_schema(200)
assert %{"error" => "Account is not found"} =
conn
|> assign(:user, user)
|> get("/api/v1/notifications?account_id=cofe")
|> json_response_and_validate_schema(404)
end
end
defp get_notification_id_by_activity(%{id: id}) do
Notification
|> Repo.get_by(activity_id: id)
|> Map.get(:id)
|> to_string()
end
defp params_to_query(%{} = params) do
Enum.map_join(params, "&", fn
{k, v} when is_list(v) -> Enum.map_join(v, "&", &"#{k}[]=#{&1}")
{k, v} -> k <> "=" <> v
end)
end
end
diff --git a/test/web/mastodon_api/views/account_view_test.exs b/test/web/mastodon_api/views/account_view_test.exs
index 8f37efa3c..c34cbcfc1 100644
--- a/test/web/mastodon_api/views/account_view_test.exs
+++ b/test/web/mastodon_api/views/account_view_test.exs
@@ -1,570 +1,570 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
use Pleroma.DataCase
alias Pleroma.Config
alias Pleroma.User
alias Pleroma.UserRelationship
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.AccountView
import Pleroma.Factory
import Tesla.Mock
setup do
mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
setup do: clear_config([:instances_favicons, :enabled])
test "Represent a user account" do
background_image = %{
"url" => [%{"href" => "https://example.com/images/asuka_hospital.png"}]
}
user =
insert(:user, %{
follower_count: 3,
note_count: 5,
background: background_image,
nickname: "shp@shitposter.club",
name: ":karjalanpiirakka: shp",
bio:
"<script src=\"invalid-html\"></script><span>valid html</span>. a<br>b<br/>c<br >d<br />f '&<>\"",
inserted_at: ~N[2017-08-15 15:47:06.597036],
emoji: %{"karjalanpiirakka" => "/file.png"},
raw_bio: "valid html. a\nb\nc\nd\nf '&<>\""
})
expected = %{
id: to_string(user.id),
username: "shp",
acct: user.nickname,
display_name: user.name,
locked: false,
created_at: "2017-08-15T15:47:06.000Z",
followers_count: 3,
following_count: 0,
statuses_count: 5,
note: "<span>valid html</span>. a<br/>b<br/>c<br/>d<br/>f &#39;&amp;&lt;&gt;&quot;",
url: user.ap_id,
avatar: "http://localhost:4001/images/avi.png",
avatar_static: "http://localhost:4001/images/avi.png",
header: "http://localhost:4001/images/banner.png",
header_static: "http://localhost:4001/images/banner.png",
emojis: [
%{
static_url: "/file.png",
url: "/file.png",
shortcode: "karjalanpiirakka",
visible_in_picker: false
}
],
fields: [],
bot: false,
source: %{
note: "valid html. a\nb\nc\nd\nf '&<>\"",
sensitive: false,
pleroma: %{
actor_type: "Person",
discoverable: false
},
fields: []
},
pleroma: %{
ap_id: user.ap_id,
background_image: "https://example.com/images/asuka_hospital.png",
favicon:
"https://shitposter.club/plugins/Qvitter/img/gnusocial-favicons/favicon-16x16.png",
confirmation_pending: false,
tags: [],
is_admin: false,
is_moderator: false,
hide_favorites: true,
hide_followers: false,
hide_follows: false,
hide_followers_count: false,
hide_follows_count: false,
relationship: %{},
skip_thread_containment: false,
accepts_chat_messages: nil
}
}
assert expected == AccountView.render("show.json", %{user: user, skip_visibility_check: true})
end
test "Favicon is nil when :instances_favicons is disabled" do
user = insert(:user)
Config.put([:instances_favicons, :enabled], true)
assert %{
pleroma: %{
favicon:
"https://shitposter.club/plugins/Qvitter/img/gnusocial-favicons/favicon-16x16.png"
}
} = AccountView.render("show.json", %{user: user, skip_visibility_check: true})
Config.put([:instances_favicons, :enabled], false)
assert %{pleroma: %{favicon: nil}} =
AccountView.render("show.json", %{user: user, skip_visibility_check: true})
end
test "Represent the user account for the account owner" do
user = insert(:user)
notification_settings = %{
block_from_strangers: false,
hide_notification_contents: false
}
privacy = user.default_scope
assert %{
pleroma: %{notification_settings: ^notification_settings, allow_following_move: true},
source: %{privacy: ^privacy}
} = AccountView.render("show.json", %{user: user, for: user})
end
test "Represent a Service(bot) account" do
user =
insert(:user, %{
follower_count: 3,
note_count: 5,
actor_type: "Service",
nickname: "shp@shitposter.club",
inserted_at: ~N[2017-08-15 15:47:06.597036]
})
expected = %{
id: to_string(user.id),
username: "shp",
acct: user.nickname,
display_name: user.name,
locked: false,
created_at: "2017-08-15T15:47:06.000Z",
followers_count: 3,
following_count: 0,
statuses_count: 5,
note: user.bio,
url: user.ap_id,
avatar: "http://localhost:4001/images/avi.png",
avatar_static: "http://localhost:4001/images/avi.png",
header: "http://localhost:4001/images/banner.png",
header_static: "http://localhost:4001/images/banner.png",
emojis: [],
fields: [],
bot: true,
source: %{
note: user.bio,
sensitive: false,
pleroma: %{
actor_type: "Service",
discoverable: false
},
fields: []
},
pleroma: %{
ap_id: user.ap_id,
background_image: nil,
favicon:
"https://shitposter.club/plugins/Qvitter/img/gnusocial-favicons/favicon-16x16.png",
confirmation_pending: false,
tags: [],
is_admin: false,
is_moderator: false,
hide_favorites: true,
hide_followers: false,
hide_follows: false,
hide_followers_count: false,
hide_follows_count: false,
relationship: %{},
skip_thread_containment: false,
accepts_chat_messages: nil
}
}
assert expected == AccountView.render("show.json", %{user: user, skip_visibility_check: true})
end
test "Represent a Funkwhale channel" do
{:ok, user} =
User.get_or_fetch_by_ap_id(
"https://channels.tests.funkwhale.audio/federation/actors/compositions"
)
assert represented =
AccountView.render("show.json", %{user: user, skip_visibility_check: true})
assert represented.acct == "compositions@channels.tests.funkwhale.audio"
assert represented.url == "https://channels.tests.funkwhale.audio/channels/compositions"
end
test "Represent a deactivated user for an admin" do
admin = insert(:user, is_admin: true)
deactivated_user = insert(:user, deactivated: true)
represented = AccountView.render("show.json", %{user: deactivated_user, for: admin})
assert represented[:pleroma][:deactivated] == true
end
test "Represent a smaller mention" do
user = insert(:user)
expected = %{
id: to_string(user.id),
acct: user.nickname,
username: user.nickname,
url: user.ap_id
}
assert expected == AccountView.render("mention.json", %{user: user})
end
test "demands :for or :skip_visibility_check option for account rendering" do
clear_config([:restrict_unauthenticated, :profiles, :local], false)
user = insert(:user)
user_id = user.id
assert %{id: ^user_id} = AccountView.render("show.json", %{user: user, for: nil})
assert %{id: ^user_id} = AccountView.render("show.json", %{user: user, for: user})
assert %{id: ^user_id} =
AccountView.render("show.json", %{user: user, skip_visibility_check: true})
assert_raise RuntimeError, ~r/:skip_visibility_check or :for option is required/, fn ->
AccountView.render("show.json", %{user: user})
end
end
describe "relationship" do
defp test_relationship_rendering(user, other_user, expected_result) do
opts = %{user: user, target: other_user, relationships: nil}
assert expected_result == AccountView.render("relationship.json", opts)
relationships_opt = UserRelationship.view_relationships_option(user, [other_user])
opts = Map.put(opts, :relationships, relationships_opt)
assert expected_result == AccountView.render("relationship.json", opts)
assert [expected_result] ==
AccountView.render("relationships.json", %{user: user, targets: [other_user]})
end
@blank_response %{
following: false,
followed_by: false,
blocking: false,
blocked_by: false,
muting: false,
muting_notifications: false,
subscribing: false,
requested: false,
domain_blocking: false,
showing_reblogs: true,
endorsed: false
}
test "represent a relationship for the following and followed user" do
user = insert(:user)
other_user = insert(:user)
{:ok, user} = User.follow(user, other_user)
{:ok, other_user} = User.follow(other_user, user)
{:ok, _subscription} = User.subscribe(user, other_user)
- {:ok, _user_relationships} = User.mute(user, other_user, true)
+ {:ok, _user_relationships} = User.mute(user, other_user, %{notifications: true})
{:ok, _reblog_mute} = CommonAPI.hide_reblogs(user, other_user)
expected =
Map.merge(
@blank_response,
%{
following: true,
followed_by: true,
muting: true,
muting_notifications: true,
subscribing: true,
showing_reblogs: false,
id: to_string(other_user.id)
}
)
test_relationship_rendering(user, other_user, expected)
end
test "represent a relationship for the blocking and blocked user" do
user = insert(:user)
other_user = insert(:user)
{:ok, user} = User.follow(user, other_user)
{:ok, _subscription} = User.subscribe(user, other_user)
{:ok, _user_relationship} = User.block(user, other_user)
{:ok, _user_relationship} = User.block(other_user, user)
expected =
Map.merge(
@blank_response,
%{following: false, blocking: true, blocked_by: true, id: to_string(other_user.id)}
)
test_relationship_rendering(user, other_user, expected)
end
test "represent a relationship for the user blocking a domain" do
user = insert(:user)
other_user = insert(:user, ap_id: "https://bad.site/users/other_user")
{:ok, user} = User.block_domain(user, "bad.site")
expected =
Map.merge(
@blank_response,
%{domain_blocking: true, blocking: false, id: to_string(other_user.id)}
)
test_relationship_rendering(user, other_user, expected)
end
test "represent a relationship for the user with a pending follow request" do
user = insert(:user)
other_user = insert(:user, locked: true)
{:ok, user, other_user, _} = CommonAPI.follow(user, other_user)
user = User.get_cached_by_id(user.id)
other_user = User.get_cached_by_id(other_user.id)
expected =
Map.merge(
@blank_response,
%{requested: true, following: false, id: to_string(other_user.id)}
)
test_relationship_rendering(user, other_user, expected)
end
end
test "returns the settings store if the requesting user is the represented user and it's requested specifically" do
user = insert(:user, pleroma_settings_store: %{fe: "test"})
result =
AccountView.render("show.json", %{user: user, for: user, with_pleroma_settings: true})
assert result.pleroma.settings_store == %{:fe => "test"}
result = AccountView.render("show.json", %{user: user, for: nil, with_pleroma_settings: true})
assert result.pleroma[:settings_store] == nil
result = AccountView.render("show.json", %{user: user, for: user})
assert result.pleroma[:settings_store] == nil
end
test "doesn't sanitize display names" do
user = insert(:user, name: "<marquee> username </marquee>")
result = AccountView.render("show.json", %{user: user, skip_visibility_check: true})
assert result.display_name == "<marquee> username </marquee>"
end
test "never display nil user follow counts" do
user = insert(:user, following_count: 0, follower_count: 0)
result = AccountView.render("show.json", %{user: user, skip_visibility_check: true})
assert result.following_count == 0
assert result.followers_count == 0
end
describe "hiding follows/following" do
test "shows when follows/followers stats are hidden and sets follow/follower count to 0" do
user =
insert(:user, %{
hide_followers: true,
hide_followers_count: true,
hide_follows: true,
hide_follows_count: true
})
other_user = insert(:user)
{:ok, user, other_user, _activity} = CommonAPI.follow(user, other_user)
{:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user)
assert %{
followers_count: 0,
following_count: 0,
pleroma: %{hide_follows_count: true, hide_followers_count: true}
} = AccountView.render("show.json", %{user: user, skip_visibility_check: true})
end
test "shows when follows/followers are hidden" do
user = insert(:user, hide_followers: true, hide_follows: true)
other_user = insert(:user)
{:ok, user, other_user, _activity} = CommonAPI.follow(user, other_user)
{:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user)
assert %{
followers_count: 1,
following_count: 1,
pleroma: %{hide_follows: true, hide_followers: true}
} = AccountView.render("show.json", %{user: user, skip_visibility_check: true})
end
test "shows actual follower/following count to the account owner" do
user = insert(:user, hide_followers: true, hide_follows: true)
other_user = insert(:user)
{:ok, user, other_user, _activity} = CommonAPI.follow(user, other_user)
assert User.following?(user, other_user)
assert Pleroma.FollowingRelationship.follower_count(other_user) == 1
{:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user)
assert %{
followers_count: 1,
following_count: 1
} = AccountView.render("show.json", %{user: user, for: user})
end
test "shows unread_conversation_count only to the account owner" do
user = insert(:user)
other_user = insert(:user)
{:ok, _activity} =
CommonAPI.post(other_user, %{
status: "Hey @#{user.nickname}.",
visibility: "direct"
})
user = User.get_cached_by_ap_id(user.ap_id)
assert AccountView.render("show.json", %{user: user, for: other_user})[:pleroma][
:unread_conversation_count
] == nil
assert AccountView.render("show.json", %{user: user, for: user})[:pleroma][
:unread_conversation_count
] == 1
end
test "shows unread_count only to the account owner" do
user = insert(:user)
insert_list(7, :notification, user: user)
other_user = insert(:user)
user = User.get_cached_by_ap_id(user.ap_id)
assert AccountView.render(
"show.json",
%{user: user, for: other_user}
)[:pleroma][:unread_notifications_count] == nil
assert AccountView.render(
"show.json",
%{user: user, for: user}
)[:pleroma][:unread_notifications_count] == 7
end
end
describe "follow requests counter" do
test "shows zero when no follow requests are pending" do
user = insert(:user)
assert %{follow_requests_count: 0} =
AccountView.render("show.json", %{user: user, for: user})
other_user = insert(:user)
{:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user)
assert %{follow_requests_count: 0} =
AccountView.render("show.json", %{user: user, for: user})
end
test "shows non-zero when follow requests are pending" do
user = insert(:user, locked: true)
assert %{locked: true} = AccountView.render("show.json", %{user: user, for: user})
other_user = insert(:user)
{:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user)
assert %{locked: true, follow_requests_count: 1} =
AccountView.render("show.json", %{user: user, for: user})
end
test "decreases when accepting a follow request" do
user = insert(:user, locked: true)
assert %{locked: true} = AccountView.render("show.json", %{user: user, for: user})
other_user = insert(:user)
{:ok, other_user, user, _activity} = CommonAPI.follow(other_user, user)
assert %{locked: true, follow_requests_count: 1} =
AccountView.render("show.json", %{user: user, for: user})
{:ok, _other_user} = CommonAPI.accept_follow_request(other_user, user)
assert %{locked: true, follow_requests_count: 0} =
AccountView.render("show.json", %{user: user, for: user})
end
test "decreases when rejecting a follow request" do
user = insert(:user, locked: true)
assert %{locked: true} = AccountView.render("show.json", %{user: user, for: user})
other_user = insert(:user)
{:ok, other_user, user, _activity} = CommonAPI.follow(other_user, user)
assert %{locked: true, follow_requests_count: 1} =
AccountView.render("show.json", %{user: user, for: user})
{:ok, _other_user} = CommonAPI.reject_follow_request(other_user, user)
assert %{locked: true, follow_requests_count: 0} =
AccountView.render("show.json", %{user: user, for: user})
end
test "shows non-zero when historical unapproved requests are present" do
user = insert(:user, locked: true)
assert %{locked: true} = AccountView.render("show.json", %{user: user, for: user})
other_user = insert(:user)
{:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user)
{:ok, user} = User.update_and_set_cache(user, %{locked: false})
assert %{locked: false, follow_requests_count: 1} =
AccountView.render("show.json", %{user: user, for: user})
end
end
test "uses mediaproxy urls when it's enabled" do
clear_config([:media_proxy, :enabled], true)
user =
insert(:user,
avatar: %{"url" => [%{"href" => "https://evil.website/avatar.png"}]},
banner: %{"url" => [%{"href" => "https://evil.website/banner.png"}]},
emoji: %{"joker_smile" => "https://evil.website/society.png"}
)
AccountView.render("show.json", %{user: user, skip_visibility_check: true})
|> Enum.all?(fn
{key, url} when key in [:avatar, :avatar_static, :header, :header_static] ->
String.starts_with?(url, Pleroma.Web.base_url())
{:emojis, emojis} ->
Enum.all?(emojis, fn %{url: url, static_url: static_url} ->
String.starts_with?(url, Pleroma.Web.base_url()) &&
String.starts_with?(static_url, Pleroma.Web.base_url())
end)
_ ->
true
end)
|> assert()
end
end

File Metadata

Mime Type
text/x-diff
Expires
Sun, Nov 1, 1:59 AM (17 h, 30 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1802196
Default Alt Text
(288 KB)

Event Timeline