Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F5811200
user.ex
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
68 KB
Referenced Files
None
Subscribers
None
user.ex
View Options
# 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
@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
)
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
(
: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
@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
{
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
:
local
})
do
config_key
=
if
local
,
do
:
:local
,
else
:
:remote
Config
.
get
([
:restrict_unauthenticated
,
:profiles
,
config_key
],
false
)
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
defdelegate
following_count
(
user
),
to
:
FollowingRelationship
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
)
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
)
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
struct
|>
confirmation_changeset
(
need_confirmation
:
need_confirmation?
)
|>
cast
(
params
,
[
:bio
,
:raw_bio
,
:email
,
:name
,
:nickname
,
:password
,
:password_confirmation
,
:emoji
,
:accepts_chat_messages
])
|>
validate_required
([
:name
,
:nickname
,
:password
,
:password_confirmation
])
|>
validate_confirmation
(
:password
)
|>
unique_constraint
(
:email
)
|>
unique_constraint
(
:nickname
)
|>
validate_exclusion
(
:nickname
,
Config
.
get
([
User
,
:restricted_nicknames
]))
|>
validate_format
(
:nickname
,
local_nickname_regex
())
|>
validate_format
(
:email
,
@email_regex
)
|>
validate_length
(
:bio
,
max
:
bio_limit
)
|>
validate_length
(
:name
,
min
:
1
,
max
:
name_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
,
_
}
<-
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_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
}
def
try_send_confirmation_email
(%
User
{}
=
user
)
do
if
user
.
confirmation_pending
&&
Config
.
get
([
:instance
,
:account_activation_required
])
do
user
|>
Pleroma.Emails.UserEmail
.
account_confirmation_email
()
|>
Pleroma.Emails.Mailer
.
deliver_async
()
{
:ok
,
:enqueued
}
else
{
:ok
,
:noop
}
end
end
def
try_send_confirmation_email
(
users
)
do
Enum
.
each
(
users
,
&
try_send_confirmation_email
/
1
)
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
defdelegate
following
(
user
),
to
:
FollowingRelationship
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
defdelegate
following?
(
follower
,
followed
),
to
:
FollowingRelationship
@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
defdelegate
get_follow_requests
(
user
),
to
:
FollowingRelationship
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
())
::
{
:ok
,
list
(
UserRelationship
.
t
())}
|
{
:error
,
String
.
t
()}
def
mute
(%
User
{}
=
muter
,
%
User
{}
=
mutee
,
notifications?
\\
true
)
do
add_to_mutes
(
muter
,
mutee
,
notifications?
)
end
def
unmute
(%
User
{}
=
muter
,
%
User
{}
=
mutee
)
do
remove_from_mutes
(
muter
,
mutee
)
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
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
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
)
if
status
==
:confirmation_pending
do
delete_and_invalidate_cache
(
user
)
else
user
|>
change
(%{
deactivated
:
true
,
email
:
nil
})
|>
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
defdelegate
search
(
query
,
opts
\\
[]),
to
:
User.Search
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
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
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Thu, Aug 14, 3:29 AM (1 d, 19 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
386434
Default Alt Text
user.ex (68 KB)
Attached To
Mode
rPUBE pleroma-upstream
Attached
Detach File
Event Timeline
Log In to Comment