Page MenuHomePhorge

No OneTemporary

Authored By
Unknown
Size
96 KB
Referenced Files
None
Subscribers
None
diff --git a/lib/pleroma/emails/user_email.ex b/lib/pleroma/emails/user_email.ex
index 7e3e9b020..856816386 100644
--- a/lib/pleroma/emails/user_email.ex
+++ b/lib/pleroma/emails/user_email.ex
@@ -1,66 +1,88 @@
defmodule Pleroma.UserEmail do
@moduledoc "User emails"
import Swoosh.Email
alias Pleroma.Web.{Endpoint, Router}
defp instance_config, do: Pleroma.Config.get(:instance)
defp instance_name, do: instance_config()[:name]
defp sender do
{instance_name(), instance_config()[:email]}
end
defp recipient(email, nil), do: email
defp recipient(email, name), do: {name, email}
+ defp recipient(%Pleroma.User{} = user), do: recipient(user.email, user.name)
def password_reset_email(user, password_reset_token) when is_binary(password_reset_token) do
password_reset_url =
Router.Helpers.util_url(
Endpoint,
:show_password_reset,
password_reset_token
)
html_body = """
<h3>Reset your password at #{instance_name()}</h3>
<p>Someone has requested password change for your account at #{instance_name()}.</p>
<p>If it was you, visit the following link to proceed: <a href="#{password_reset_url}">reset password</a>.</p>
<p>If it was someone else, nothing to worry about: your data is secure and your password has not been changed.</p>
"""
new()
- |> to(recipient(user.email, user.name))
+ |> to(recipient(user))
|> from(sender())
|> subject("Password reset")
|> html_body(html_body)
end
def user_invitation_email(
user,
%Pleroma.UserInviteToken{} = user_invite_token,
to_email,
to_name \\ nil
) do
registration_url =
Router.Helpers.redirect_url(
Endpoint,
:registration_page,
user_invite_token.token
)
html_body = """
<h3>You are invited to #{instance_name()}</h3>
<p>#{user.name} invites you to join #{instance_name()}, an instance of Pleroma federated social networking platform.</p>
<p>Click the following link to register: <a href="#{registration_url}">accept invitation</a>.</p>
"""
new()
|> to(recipient(to_email, to_name))
|> from(sender())
|> subject("Invitation to #{instance_name()}")
|> html_body(html_body)
end
+
+ def account_confirmation_email(user) do
+ confirmation_url =
+ Router.Helpers.confirm_email_url(
+ Endpoint,
+ :confirm_email,
+ to_string(user.info.confirmation_token)
+ )
+
+ html_body = """
+ <h3>Welcome to #{instance_name()}!</h3>
+ <p>Email confirmation is required to activate the account.</p>
+ <p>Click the following link to proceed: <a href="#{confirmation_url}">activate your account</a>.</p>
+ """
+
+ new()
+ |> to(recipient(user))
+ |> from(sender())
+ |> subject("#{instance_name()} account confirmation")
+ |> html_body(html_body)
+ end
end
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index ee0a0dfb9..a38ead81a 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -1,881 +1,883 @@
defmodule Pleroma.User do
use Ecto.Schema
import Ecto.{Changeset, Query}
alias Pleroma.{Repo, User, Object, Web, Activity, Notification}
alias Comeonin.Pbkdf2
alias Pleroma.Formatter
alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
alias Pleroma.Web.{OStatus, Websub, OAuth}
alias Pleroma.Web.ActivityPub.{Utils, ActivityPub}
@type t :: %__MODULE__{}
@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_-]+$/
schema "users" do
field(: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(:following, {:array, :string}, default: [])
field(:ap_id, :string)
field(:avatar, :map)
field(:local, :boolean, default: true)
field(:follower_address, :string)
field(:search_distance, :float, virtual: true)
field(:tags, {:array, :string}, default: [])
field(:last_refreshed_at, :naive_datetime)
has_many(:notifications, Notification)
embeds_one(:info, Pleroma.User.Info)
timestamps()
end
+ def auth_active?(user), do: user.info && !user.info.confirmation_pending
+
def avatar_url(user) do
case user.avatar do
%{"url" => [%{"href" => href} | _]} -> href
_ -> "#{Web.base_url()}/images/avi.png"
end
end
def banner_url(user) do
case user.info.banner do
%{"url" => [%{"href" => href} | _]} -> href
_ -> "#{Web.base_url()}/images/banner.png"
end
end
def profile_url(%User{info: %{source_data: %{"url" => url}}}), do: url
def profile_url(%User{ap_id: ap_id}), do: ap_id
def profile_url(_), do: nil
def ap_id(%User{nickname: nickname}) do
"#{Web.base_url()}/users/#{nickname}"
end
def ap_followers(%User{} = user) do
"#{ap_id(user)}/followers"
end
def follow_changeset(struct, params \\ %{}) do
struct
|> cast(params, [:following])
|> validate_required([:following])
end
def user_info(%User{} = user) do
oneself = if user.local, do: 1, else: 0
%{
following_count: length(user.following) - oneself,
note_count: user.info.note_count,
follower_count: user.info.follower_count,
locked: user.info.locked,
default_scope: user.info.default_scope
}
end
def remote_user_creation(params) do
params =
params
|> Map.put(:info, params[:info] || %{})
info_cng = User.Info.remote_user_creation(%User.Info{}, params[:info])
changes =
%User{}
|> cast(params, [:bio, :name, :ap_id, :nickname, :avatar])
|> validate_required([:name, :ap_id])
|> unique_constraint(:nickname)
|> validate_format(:nickname, @email_regex)
|> validate_length(:bio, max: 5000)
|> validate_length(:name, max: 100)
|> put_change(:local, false)
|> put_embed(:info, info_cng)
if changes.valid? do
case info_cng.changes[:source_data] do
%{"followers" => followers} ->
changes
|> put_change(:follower_address, followers)
_ ->
followers = User.ap_followers(%User{nickname: changes.changes[:nickname]})
changes
|> put_change(:follower_address, followers)
end
else
changes
end
end
def update_changeset(struct, params \\ %{}) do
struct
|> cast(params, [:bio, :name, :avatar])
|> unique_constraint(:nickname)
|> validate_format(:nickname, local_nickname_regex())
|> validate_length(:bio, max: 5000)
|> validate_length(:name, min: 1, max: 100)
end
def upgrade_changeset(struct, params \\ %{}) do
params =
params
|> Map.put(:last_refreshed_at, NaiveDateTime.utc_now())
info_cng =
struct.info
|> User.Info.user_upgrade(params[:info])
struct
|> cast(params, [:bio, :name, :follower_address, :avatar, :last_refreshed_at])
|> unique_constraint(:nickname)
|> validate_format(:nickname, local_nickname_regex())
|> validate_length(:bio, max: 5000)
|> validate_length(:name, max: 100)
|> put_embed(:info, info_cng)
end
def password_update_changeset(struct, params) do
changeset =
struct
|> cast(params, [:password, :password_confirmation])
|> validate_required([:password, :password_confirmation])
|> validate_confirmation(:password)
OAuth.Token.delete_user_tokens(struct)
OAuth.Authorization.delete_user_authorizations(struct)
if changeset.valid? do
hashed = Pbkdf2.hashpwsalt(changeset.changes[:password])
changeset
|> put_change(:password_hash, hashed)
else
changeset
end
end
def reset_password(user, data) do
update_and_set_cache(password_update_changeset(user, data))
end
def register_changeset(struct, params \\ %{}) do
changeset =
struct
|> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
|> validate_required([:email, :name, :nickname, :password, :password_confirmation])
|> validate_confirmation(:password)
|> unique_constraint(:email)
|> unique_constraint(:nickname)
|> validate_format(:nickname, local_nickname_regex())
|> validate_format(:email, @email_regex)
|> validate_length(:bio, max: 1000)
|> validate_length(:name, min: 1, max: 100)
|> put_change(:info, %Pleroma.User.Info{})
if changeset.valid? do
hashed = Pbkdf2.hashpwsalt(changeset.changes[:password])
ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]})
followers = User.ap_followers(%User{nickname: changeset.changes[:nickname]})
changeset
|> put_change(:password_hash, hashed)
|> put_change(:ap_id, ap_id)
|> put_change(:following, [followers])
|> put_change(:follower_address, followers)
else
changeset
end
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) >= 86400
end
def needs_update?(_), do: true
def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
{:ok, follower}
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 User.ap_enabled?(followed) do
follow(follower, followed)
else
{:ok, follower}
end
end
def maybe_follow(%User{} = follower, %User{info: _info} = followed) do
if not following?(follower, followed) do
follow(follower, followed)
else
{:ok, follower}
end
end
def follow(%User{} = follower, %User{info: info} = followed) do
user_config = Application.get_env(:pleroma, :user)
deny_follow_blocked = Keyword.get(user_config, :deny_follow_blocked)
ap_followers = followed.follower_address
cond do
following?(follower, followed) or info.deactivated ->
{:error, "Could not follow user: #{followed.nickname} is already on your list."}
deny_follow_blocked and blocks?(followed, follower) ->
{:error, "Could not follow user: #{followed.nickname} blocked you."}
true ->
if !followed.local && follower.local && !ap_enabled?(followed) do
Websub.subscribe(follower, followed)
end
following =
[ap_followers | follower.following]
|> Enum.uniq()
follower =
follower
|> follow_changeset(%{following: following})
|> update_and_set_cache
{:ok, _} = update_follower_count(followed)
follower
end
end
def unfollow(%User{} = follower, %User{} = followed) do
ap_followers = followed.follower_address
if following?(follower, followed) and follower.ap_id != followed.ap_id do
following =
follower.following
|> List.delete(ap_followers)
{:ok, follower} =
follower
|> follow_changeset(%{following: following})
|> update_and_set_cache
{:ok, followed} = update_follower_count(followed)
{:ok, follower, Utils.fetch_latest_follow(follower, followed)}
else
{:error, "Not subscribed!"}
end
end
@spec following?(User.t(), User.t()) :: boolean
def following?(%User{} = follower, %User{} = followed) do
Enum.member?(follower.following, followed.follower_address)
end
def locked?(%User{} = user) do
user.info.locked || false
end
def get_by_ap_id(ap_id) do
Repo.get_by(User, ap_id: ap_id)
end
def update_and_set_cache(changeset) do
with {:ok, user} <- Repo.update(changeset) do
Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
{:ok, user}
else
e -> e
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, "user_info:#{user.id}")
end
def get_cached_by_ap_id(ap_id) do
key = "ap_id:#{ap_id}"
Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
end
def get_cached_by_nickname(nickname) do
key = "nickname:#{nickname}"
Cachex.fetch!(:user_cache, key, fn _ -> get_or_fetch_by_nickname(nickname) end)
end
def get_by_nickname(nickname) do
Repo.get_by(User, nickname: nickname)
end
def get_by_nickname_or_email(nickname_or_email) do
case user = Repo.get_by(User, nickname: nickname_or_email) do
%User{} -> user
nil -> Repo.get_by(User, email: nickname_or_email)
end
end
def get_cached_user_info(user) do
key = "user_info:#{user.id}"
Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
end
def fetch_by_nickname(nickname) do
ap_try = ActivityPub.make_user_from_nickname(nickname)
case ap_try do
{:ok, user} -> {:ok, user}
_ -> OStatus.make_user(nickname)
end
end
def get_or_fetch_by_nickname(nickname) do
with %User{} = user <- get_by_nickname(nickname) do
user
else
_e ->
with [_nick, _domain] <- String.split(nickname, "@"),
{:ok, user} <- fetch_by_nickname(nickname) do
user
else
_e -> nil
end
end
end
def get_by_confirmation_token(token) do
Repo.one(from(u in User, where: fragment("? ->> 'confirmation_token' = ?", u.info, ^token)))
end
def get_followers_query(%User{id: id, follower_address: follower_address}) do
from(
u in User,
where: fragment("? <@ ?", ^[follower_address], u.following),
where: u.id != ^id
)
end
def get_followers(user) do
q = get_followers_query(user)
{:ok, Repo.all(q)}
end
def get_friends_query(%User{id: id, following: following}) do
from(
u in User,
where: u.follower_address in ^following,
where: u.id != ^id
)
end
def get_friends(user) do
q = get_friends_query(user)
{:ok, Repo.all(q)}
end
def get_follow_requests_query(%User{} = user) do
from(
a in Activity,
where:
fragment(
"? ->> 'type' = 'Follow'",
a.data
),
where:
fragment(
"? ->> 'state' = 'pending'",
a.data
),
where:
fragment(
"? @> ?",
a.data,
^%{"object" => user.ap_id}
)
)
end
def get_follow_requests(%User{} = user) do
q = get_follow_requests_query(user)
reqs = Repo.all(q)
users =
Enum.map(reqs, fn req -> req.actor end)
|> Enum.uniq()
|> Enum.map(fn ap_id -> get_by_ap_id(ap_id) end)
|> Enum.filter(fn u -> !following?(u, user) end)
{:ok, users}
end
def increase_note_count(%User{} = user) do
info_cng = User.Info.add_to_note_count(user.info, 1)
cng =
change(user)
|> put_embed(:info, info_cng)
update_and_set_cache(cng)
end
def decrease_note_count(%User{} = user) do
info_cng = User.Info.add_to_note_count(user.info, -1)
cng =
change(user)
|> put_embed(:info, info_cng)
update_and_set_cache(cng)
end
def update_note_count(%User{} = user) do
note_count_query =
from(
a in Object,
where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
select: count(a.id)
)
note_count = Repo.one(note_count_query)
info_cng = User.Info.set_note_count(user.info, note_count)
cng =
change(user)
|> put_embed(:info, info_cng)
update_and_set_cache(cng)
end
def update_follower_count(%User{} = user) do
follower_count_query =
from(
u in User,
where: ^user.follower_address in u.following,
where: u.id != ^user.id,
select: count(u.id)
)
follower_count = Repo.one(follower_count_query)
info_cng =
user.info
|> User.Info.set_follower_count(follower_count)
cng =
change(user)
|> put_embed(:info, info_cng)
update_and_set_cache(cng)
end
def get_users_from_set_query(ap_ids, false) do
from(
u in User,
where: u.ap_id in ^ap_ids
)
end
def get_users_from_set_query(ap_ids, true) do
query = get_users_from_set_query(ap_ids, false)
from(
u in query,
where: u.local == true
)
end
def get_users_from_set(ap_ids, local_only \\ true) do
get_users_from_set_query(ap_ids, local_only)
|> Repo.all()
end
def get_recipients_from_activity(%Activity{recipients: to}) do
query =
from(
u in User,
where: u.ap_id in ^to,
or_where: fragment("? && ?", u.following, ^to)
)
query = from(u in query, where: u.local == true)
Repo.all(query)
end
def search(query, resolve \\ false) do
# strip the beginning @ off if there is a query
query = String.trim_leading(query, "@")
if resolve do
User.get_or_fetch_by_nickname(query)
end
inner =
from(
u in User,
select_merge: %{
search_distance:
fragment(
"? <-> (? || ?)",
^query,
u.nickname,
u.name
)
},
where: not is_nil(u.nickname)
)
q =
from(
s in subquery(inner),
order_by: s.search_distance,
limit: 20
)
Repo.all(q)
end
def block(blocker, %User{ap_id: ap_id} = 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
if following?(blocked, blocker) do
unfollow(blocked, blocker)
end
info_cng =
blocker.info
|> User.Info.add_to_block(ap_id)
cng =
change(blocker)
|> put_embed(:info, info_cng)
update_and_set_cache(cng)
end
# helper to handle the block given only an actor's AP id
def block(blocker, %{ap_id: ap_id}) do
block(blocker, User.get_by_ap_id(ap_id))
end
def unblock(blocker, %{ap_id: ap_id}) do
info_cng =
blocker.info
|> User.Info.remove_from_block(ap_id)
cng =
change(blocker)
|> put_embed(:info, info_cng)
update_and_set_cache(cng)
end
def blocks?(user, %{ap_id: ap_id}) do
blocks = user.info.blocks
domain_blocks = user.info.domain_blocks
%{host: host} = URI.parse(ap_id)
Enum.member?(blocks, ap_id) ||
Enum.any?(domain_blocks, fn domain ->
host == domain
end)
end
def block_domain(user, domain) do
info_cng =
user.info
|> User.Info.add_to_domain_block(domain)
cng =
change(user)
|> put_embed(:info, info_cng)
update_and_set_cache(cng)
end
def unblock_domain(user, domain) do
info_cng =
user.info
|> User.Info.remove_from_domain_block(domain)
cng =
change(user)
|> put_embed(:info, info_cng)
update_and_set_cache(cng)
end
def local_user_query() do
from(
u in User,
where: u.local == true,
where: not is_nil(u.nickname)
)
end
def moderator_user_query() do
from(
u in User,
where: u.local == true,
where: fragment("?->'is_moderator' @> 'true'", u.info)
)
end
def deactivate(%User{} = user, status \\ true) do
info_cng = User.Info.set_activation_status(user.info, status)
cng =
change(user)
|> put_embed(:info, info_cng)
update_and_set_cache(cng)
end
def delete(%User{} = user) do
{:ok, user} = User.deactivate(user)
# Remove all relationships
{:ok, followers} = User.get_followers(user)
followers
|> Enum.each(fn follower -> User.unfollow(follower, user) end)
{:ok, friends} = User.get_friends(user)
friends
|> Enum.each(fn followed -> User.unfollow(user, followed) end)
query = from(a in Activity, where: a.actor == ^user.ap_id)
Repo.all(query)
|> Enum.each(fn activity ->
case activity.data["type"] do
"Create" ->
ActivityPub.delete(Object.normalize(activity.data["object"]))
# TODO: Do something with likes, follows, repeats.
_ ->
"Doing nothing"
end
end)
{:ok, user}
end
def html_filter_policy(%User{info: %{no_rich_text: true}}) do
Pleroma.HTML.Scrubber.TwitterText
end
def html_filter_policy(_), do: nil
def get_or_fetch_by_ap_id(ap_id) do
user = get_by_ap_id(ap_id)
if !is_nil(user) and !User.needs_update?(user) do
user
else
ap_try = ActivityPub.make_user_from_ap_id(ap_id)
case ap_try do
{:ok, user} ->
user
_ ->
case OStatus.make_user(ap_id) do
{:ok, user} -> user
_ -> {:error, "Could not fetch by AP id"}
end
end
end
end
def get_or_create_instance_user do
relay_uri = "#{Pleroma.Web.Endpoint.url()}/relay"
if user = get_by_ap_id(relay_uri) do
user
else
changes =
%User{info: %User.Info{}}
|> cast(%{}, [:ap_id, :nickname, :local])
|> put_change(:ap_id, relay_uri)
|> put_change(:nickname, nil)
|> put_change(:local, true)
|> put_change(:follower_address, relay_uri <> "/followers")
{:ok, user} = Repo.insert(changes)
user
end
end
# AP style
def public_key_from_info(%{
source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
}) do
key =
public_key_pem
|> :public_key.pem_decode()
|> hd()
|> :public_key.pem_entry_decode()
{:ok, key}
end
# OStatus Magic Key
def public_key_from_info(%{magic_key: magic_key}) do
{:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
end
def get_public_key_for_ap_id(ap_id) do
with %User{} = user <- get_or_fetch_by_ap_id(ap_id),
{:ok, public_key} <- public_key_from_info(user.info) do
{:ok, public_key}
else
_ -> :error
end
end
defp blank?(""), do: nil
defp blank?(n), do: n
def insert_or_update_user(data) do
data =
data
|> Map.put(:name, blank?(data[:name]) || data[:nickname])
cs = User.remote_user_creation(data)
Repo.insert(cs, on_conflict: :replace_all, conflict_target: :nickname)
end
def ap_enabled?(%User{local: true}), do: true
def ap_enabled?(%User{info: info}), do: info.ap_enabled
def ap_enabled?(_), do: false
@doc "Gets or fetch a user by uri or nickname."
@spec get_or_fetch(String.t()) :: User.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 <- Repo.get(User, a.id),
%User{} = b <- Repo.get(User, b.id) do
{:ok, a, b}
else
_e ->
:error
end
end
def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
with :ok <- :timer.sleep(timeout),
%User{} = a <- Repo.get(User, a.id),
%User{} = b <- Repo.get(User, b.id) do
{:ok, a, b}
else
_e ->
:error
end
end
def parse_bio(bio, user \\ %User{info: %{source_data: %{}}})
def parse_bio(nil, _user), do: ""
def parse_bio(bio, _user) when bio == "", do: bio
def parse_bio(bio, user) do
mentions = Formatter.parse_mentions(bio)
tags = Formatter.parse_tags(bio)
emoji =
(user.info.source_data["tag"] || [])
|> Enum.filter(fn %{"type" => t} -> t == "Emoji" end)
|> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} ->
{String.trim(name, ":"), url}
end)
bio
|> CommonUtils.format_input(mentions, tags, "text/plain")
|> Formatter.emojify(emoji)
end
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(User.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(User.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})
|> Repo.update()
updated_user
end
defp normalize_tags(tags) do
[tags]
|> List.flatten()
|> Enum.map(&String.downcase(&1))
end
defp local_nickname_regex() do
if Pleroma.Config.get([:instance, :extended_nickname_format]) do
@extended_local_nickname_regex
else
@strict_local_nickname_regex
end
end
end
diff --git a/lib/pleroma/user/info.ex b/lib/pleroma/user/info.ex
index f75984038..9ce9129cd 100644
--- a/lib/pleroma/user/info.ex
+++ b/lib/pleroma/user/info.ex
@@ -1,181 +1,195 @@
defmodule Pleroma.User.Info do
use Ecto.Schema
import Ecto.Changeset
embedded_schema do
field(:banner, :map, default: %{})
field(:background, :map, default: %{})
field(:source_data, :map, default: %{})
field(:note_count, :integer, default: 0)
field(:follower_count, :integer, default: 0)
field(:locked, :boolean, default: false)
field(:confirmation_pending, :boolean, default: false)
field(:confirmation_token, :string, default: nil)
field(:default_scope, :string, default: "public")
field(:blocks, {:array, :string}, default: [])
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(:keys, :string, default: nil)
field(:settings, :map, default: nil)
field(:magic_key, :string, default: nil)
field(:uri, :string, default: nil)
field(:topic, :string, default: nil)
field(:hub, :string, default: nil)
field(:salmon, :string, default: nil)
field(:hide_network, :boolean, default: false)
# Found in the wild
# ap_id -> Where is this used?
# bio -> Where is this used?
# avatar -> Where is this used?
# fqn -> Where is this used?
# host -> Where is this used?
# subject _> Where is this used?
end
def set_activation_status(info, deactivated) do
params = %{deactivated: deactivated}
info
|> cast(params, [:deactivated])
|> validate_required([:deactivated])
end
def add_to_note_count(info, number) do
set_note_count(info, info.note_count + number)
end
def set_note_count(info, number) do
params = %{note_count: Enum.max([0, number])}
info
|> cast(params, [:note_count])
|> validate_required([:note_count])
end
def set_follower_count(info, number) do
params = %{follower_count: Enum.max([0, number])}
info
|> cast(params, [:follower_count])
|> validate_required([:follower_count])
end
def set_blocks(info, blocks) do
params = %{blocks: blocks}
info
|> cast(params, [:blocks])
|> validate_required([:blocks])
end
def add_to_block(info, blocked) do
set_blocks(info, Enum.uniq([blocked | info.blocks]))
end
def remove_from_block(info, blocked) do
set_blocks(info, List.delete(info.blocks, blocked))
end
def set_domain_blocks(info, domain_blocks) do
params = %{domain_blocks: domain_blocks}
info
|> cast(params, [:domain_blocks])
|> validate_required([:domain_blocks])
end
def add_to_domain_block(info, domain_blocked) do
set_domain_blocks(info, Enum.uniq([domain_blocked | info.domain_blocks]))
end
def remove_from_domain_block(info, domain_blocked) do
set_domain_blocks(info, List.delete(info.domain_blocks, domain_blocked))
end
def set_keys(info, keys) do
params = %{keys: keys}
info
|> cast(params, [:keys])
|> validate_required([:keys])
end
def remote_user_creation(info, params) do
info
|> cast(params, [
:ap_enabled,
:source_data,
:banner,
:locked,
:magic_key,
:uri,
:hub,
:topic,
:salmon
])
end
def user_upgrade(info, params) do
info
|> cast(params, [
:ap_enabled,
:source_data,
:banner,
:locked,
:magic_key
])
end
def profile_update(info, params) do
info
|> cast(params, [
:locked,
:no_rich_text,
:default_scope,
:banner,
:hide_network,
:background
])
end
+ def confirmation_update(info, :confirmed) do
+ confirmation_update(info, %{
+ confirmation_pending: false,
+ confirmation_token: nil
+ })
+ end
+
+ def confirmation_update(info, :unconfirmed) do
+ confirmation_update(info, %{
+ confirmation_pending: true,
+ confirmation_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64()
+ })
+ end
+
def confirmation_update(info, params) do
cast(info, params, [:confirmation_pending, :confirmation_token])
end
def mastodon_profile_update(info, params) do
info
|> cast(params, [
:locked,
:banner
])
end
def mastodon_settings_update(info, settings) do
params = %{settings: settings}
info
|> cast(params, [:settings])
|> validate_required([:settings])
end
def set_source_data(info, source_data) do
params = %{source_data: source_data}
info
|> cast(params, [:source_data])
|> validate_required([:source_data])
end
def admin_api_update(info, params) do
info
|> cast(params, [
:is_moderator,
:is_admin
])
end
end
diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex
index 20c2e799b..10158f07e 100644
--- a/lib/pleroma/web/oauth/oauth_controller.ex
+++ b/lib/pleroma/web/oauth/oauth_controller.ex
@@ -1,178 +1,180 @@
defmodule Pleroma.Web.OAuth.OAuthController do
use Pleroma.Web, :controller
alias Pleroma.Web.OAuth.{Authorization, Token, App}
alias Pleroma.{Repo, User}
alias Comeonin.Pbkdf2
plug(:fetch_session)
plug(:fetch_flash)
action_fallback(Pleroma.Web.OAuth.FallbackController)
def authorize(conn, params) do
render(conn, "show.html", %{
response_type: params["response_type"],
client_id: params["client_id"],
scope: params["scope"],
redirect_uri: params["redirect_uri"],
state: params["state"]
})
end
def create_authorization(conn, %{
"authorization" =>
%{
"name" => name,
"password" => password,
"client_id" => client_id,
"redirect_uri" => redirect_uri
} = params
}) do
with %User{} = user <- User.get_by_nickname_or_email(name),
true <- Pbkdf2.checkpw(password, user.password_hash),
+ true <- User.auth_active?(user),
%App{} = app <- Repo.get_by(App, client_id: client_id),
{:ok, auth} <- Authorization.create_authorization(app, user) do
# Special case: Local MastodonFE.
redirect_uri =
if redirect_uri == "." do
mastodon_api_url(conn, :login)
else
redirect_uri
end
cond do
redirect_uri == "urn:ietf:wg:oauth:2.0:oob" ->
render(conn, "results.html", %{
auth: auth
})
true ->
connector = if String.contains?(redirect_uri, "?"), do: "&", else: "?"
url = "#{redirect_uri}#{connector}"
url_params = %{:code => auth.token}
url_params =
if params["state"] do
Map.put(url_params, :state, params["state"])
else
url_params
end
url = "#{url}#{Plug.Conn.Query.encode(url_params)}"
redirect(conn, external: url)
end
end
end
# TODO
# - proper scope handling
def token_exchange(conn, %{"grant_type" => "authorization_code"} = params) do
with %App{} = app <- get_app_from_request(conn, params),
fixed_token = fix_padding(params["code"]),
%Authorization{} = auth <-
Repo.get_by(Authorization, token: fixed_token, app_id: app.id),
{:ok, token} <- Token.exchange_token(app, auth),
{:ok, inserted_at} <- DateTime.from_naive(token.inserted_at, "Etc/UTC") do
response = %{
token_type: "Bearer",
access_token: token.token,
refresh_token: token.refresh_token,
created_at: DateTime.to_unix(inserted_at),
expires_in: 60 * 10,
scope: "read write follow"
}
json(conn, response)
else
_error ->
put_status(conn, 400)
|> json(%{error: "Invalid credentials"})
end
end
# TODO
# - investigate a way to verify the user wants to grant read/write/follow once scope handling is done
def token_exchange(
conn,
%{"grant_type" => "password", "username" => name, "password" => password} = params
) do
with %App{} = app <- get_app_from_request(conn, params),
%User{} = user <- User.get_by_nickname_or_email(name),
true <- Pbkdf2.checkpw(password, user.password_hash),
+ true <- User.auth_active?(user),
{:ok, auth} <- Authorization.create_authorization(app, user),
{:ok, token} <- Token.exchange_token(app, auth) do
response = %{
token_type: "Bearer",
access_token: token.token,
refresh_token: token.refresh_token,
expires_in: 60 * 10,
scope: "read write follow"
}
json(conn, response)
else
_error ->
put_status(conn, 400)
|> json(%{error: "Invalid credentials"})
end
end
def token_exchange(
conn,
%{"grant_type" => "password", "name" => name, "password" => _password} = params
) do
params =
params
|> Map.delete("name")
|> Map.put("username", name)
token_exchange(conn, params)
end
def token_revoke(conn, %{"token" => token} = params) do
with %App{} = app <- get_app_from_request(conn, params),
%Token{} = token <- Repo.get_by(Token, token: token, app_id: app.id),
{:ok, %Token{}} <- Repo.delete(token) do
json(conn, %{})
else
_error ->
# RFC 7009: invalid tokens [in the request] do not cause an error response
json(conn, %{})
end
end
# XXX - for whatever reason our token arrives urlencoded, but Plug.Conn should be
# decoding it. Investigate sometime.
defp fix_padding(token) do
token
|> URI.decode()
|> Base.url_decode64!(padding: false)
|> Base.url_encode64()
end
defp get_app_from_request(conn, params) do
# Per RFC 6749, HTTP Basic is preferred to body params
{client_id, client_secret} =
with ["Basic " <> encoded] <- get_req_header(conn, "authorization"),
{:ok, decoded} <- Base.decode64(encoded),
[id, secret] <-
String.split(decoded, ":")
|> Enum.map(fn s -> URI.decode_www_form(s) end) do
{id, secret}
else
_ -> {params["client_id"], params["client_secret"]}
end
if client_id && client_secret do
Repo.get_by(
App,
client_id: client_id,
client_secret: client_secret
)
else
nil
end
end
end
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index b2fbc088d..0e4589116 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -1,475 +1,476 @@
defmodule Pleroma.Web.Router do
use Pleroma.Web, :router
pipeline :api do
plug(:accepts, ["json"])
plug(:fetch_session)
plug(Pleroma.Plugs.OAuthPlug)
plug(Pleroma.Plugs.BasicAuthDecoderPlug)
plug(Pleroma.Plugs.UserFetcherPlug)
plug(Pleroma.Plugs.SessionAuthenticationPlug)
plug(Pleroma.Plugs.LegacyAuthenticationPlug)
plug(Pleroma.Plugs.AuthenticationPlug)
plug(Pleroma.Plugs.UserEnabledPlug)
plug(Pleroma.Plugs.SetUserSessionIdPlug)
plug(Pleroma.Plugs.EnsureUserKeyPlug)
end
pipeline :authenticated_api do
plug(:accepts, ["json"])
plug(:fetch_session)
plug(Pleroma.Plugs.OAuthPlug)
plug(Pleroma.Plugs.BasicAuthDecoderPlug)
plug(Pleroma.Plugs.UserFetcherPlug)
plug(Pleroma.Plugs.SessionAuthenticationPlug)
plug(Pleroma.Plugs.LegacyAuthenticationPlug)
plug(Pleroma.Plugs.AuthenticationPlug)
plug(Pleroma.Plugs.UserEnabledPlug)
plug(Pleroma.Plugs.SetUserSessionIdPlug)
plug(Pleroma.Plugs.EnsureAuthenticatedPlug)
end
pipeline :admin_api do
plug(:accepts, ["json"])
plug(:fetch_session)
plug(Pleroma.Plugs.OAuthPlug)
plug(Pleroma.Plugs.BasicAuthDecoderPlug)
plug(Pleroma.Plugs.UserFetcherPlug)
plug(Pleroma.Plugs.SessionAuthenticationPlug)
plug(Pleroma.Plugs.LegacyAuthenticationPlug)
plug(Pleroma.Plugs.AuthenticationPlug)
plug(Pleroma.Plugs.UserEnabledPlug)
plug(Pleroma.Plugs.SetUserSessionIdPlug)
plug(Pleroma.Plugs.EnsureAuthenticatedPlug)
plug(Pleroma.Plugs.UserIsAdminPlug)
end
pipeline :mastodon_html do
plug(:accepts, ["html"])
plug(:fetch_session)
plug(Pleroma.Plugs.OAuthPlug)
plug(Pleroma.Plugs.BasicAuthDecoderPlug)
plug(Pleroma.Plugs.UserFetcherPlug)
plug(Pleroma.Plugs.SessionAuthenticationPlug)
plug(Pleroma.Plugs.LegacyAuthenticationPlug)
plug(Pleroma.Plugs.AuthenticationPlug)
plug(Pleroma.Plugs.UserEnabledPlug)
plug(Pleroma.Plugs.SetUserSessionIdPlug)
plug(Pleroma.Plugs.EnsureUserKeyPlug)
end
pipeline :pleroma_html do
plug(:accepts, ["html"])
plug(:fetch_session)
plug(Pleroma.Plugs.OAuthPlug)
plug(Pleroma.Plugs.BasicAuthDecoderPlug)
plug(Pleroma.Plugs.UserFetcherPlug)
plug(Pleroma.Plugs.SessionAuthenticationPlug)
plug(Pleroma.Plugs.AuthenticationPlug)
plug(Pleroma.Plugs.EnsureUserKeyPlug)
end
pipeline :well_known do
plug(:accepts, ["json", "jrd+json", "xml", "xrd+xml"])
end
pipeline :config do
plug(:accepts, ["json", "xml"])
end
pipeline :oauth do
plug(:accepts, ["html", "json"])
end
pipeline :pleroma_api do
plug(:accepts, ["html", "json"])
end
pipeline :mailbox_preview do
plug(:accepts, ["html"])
plug(:put_secure_browser_headers, %{
"content-security-policy" =>
"default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline' 'unsafe-eval'"
})
end
scope "/api/pleroma", Pleroma.Web.TwitterAPI do
pipe_through(:pleroma_api)
get("/password_reset/:token", UtilController, :show_password_reset)
post("/password_reset", UtilController, :password_reset)
get("/emoji", UtilController, :emoji)
get("/captcha", UtilController, :captcha)
end
scope "/api/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through(:admin_api)
delete("/user", AdminAPIController, :user_delete)
post("/user", AdminAPIController, :user_create)
put("/users/tag", AdminAPIController, :tag_users)
delete("/users/tag", AdminAPIController, :untag_users)
get("/permission_group/:nickname", AdminAPIController, :right_get)
get("/permission_group/:nickname/:permission_group", AdminAPIController, :right_get)
post("/permission_group/:nickname/:permission_group", AdminAPIController, :right_add)
delete("/permission_group/:nickname/:permission_group", AdminAPIController, :right_delete)
post("/relay", AdminAPIController, :relay_follow)
delete("/relay", AdminAPIController, :relay_unfollow)
get("/invite_token", AdminAPIController, :get_invite_token)
post("/email_invite", AdminAPIController, :email_invite)
get("/password_reset", AdminAPIController, :get_password_reset)
end
scope "/", Pleroma.Web.TwitterAPI do
pipe_through(:pleroma_html)
get("/ostatus_subscribe", UtilController, :remote_follow)
post("/ostatus_subscribe", UtilController, :do_remote_follow)
post("/main/ostatus", UtilController, :remote_subscribe)
end
scope "/api/pleroma", Pleroma.Web.TwitterAPI do
pipe_through(:authenticated_api)
post("/follow_import", UtilController, :follow_import)
post("/change_password", UtilController, :change_password)
post("/delete_account", UtilController, :delete_account)
end
scope "/oauth", Pleroma.Web.OAuth do
get("/authorize", OAuthController, :authorize)
post("/authorize", OAuthController, :create_authorization)
post("/token", OAuthController, :token_exchange)
post("/revoke", OAuthController, :token_revoke)
end
scope "/api/v1", Pleroma.Web.MastodonAPI do
pipe_through(:authenticated_api)
patch("/accounts/update_credentials", MastodonAPIController, :update_credentials)
get("/accounts/verify_credentials", MastodonAPIController, :verify_credentials)
get("/accounts/relationships", MastodonAPIController, :relationships)
get("/accounts/search", MastodonAPIController, :account_search)
post("/accounts/:id/follow", MastodonAPIController, :follow)
post("/accounts/:id/unfollow", MastodonAPIController, :unfollow)
post("/accounts/:id/block", MastodonAPIController, :block)
post("/accounts/:id/unblock", MastodonAPIController, :unblock)
post("/accounts/:id/mute", MastodonAPIController, :relationship_noop)
post("/accounts/:id/unmute", MastodonAPIController, :relationship_noop)
get("/accounts/:id/lists", MastodonAPIController, :account_lists)
get("/follow_requests", MastodonAPIController, :follow_requests)
post("/follow_requests/:id/authorize", MastodonAPIController, :authorize_follow_request)
post("/follow_requests/:id/reject", MastodonAPIController, :reject_follow_request)
post("/follows", MastodonAPIController, :follow)
get("/blocks", MastodonAPIController, :blocks)
get("/mutes", MastodonAPIController, :empty_array)
get("/timelines/home", MastodonAPIController, :home_timeline)
get("/timelines/direct", MastodonAPIController, :dm_timeline)
get("/favourites", MastodonAPIController, :favourites)
post("/statuses", MastodonAPIController, :post_status)
delete("/statuses/:id", MastodonAPIController, :delete_status)
post("/statuses/:id/reblog", MastodonAPIController, :reblog_status)
post("/statuses/:id/unreblog", MastodonAPIController, :unreblog_status)
post("/statuses/:id/favourite", MastodonAPIController, :fav_status)
post("/statuses/:id/unfavourite", MastodonAPIController, :unfav_status)
post("/notifications/clear", MastodonAPIController, :clear_notifications)
post("/notifications/dismiss", MastodonAPIController, :dismiss_notification)
get("/notifications", MastodonAPIController, :notifications)
get("/notifications/:id", MastodonAPIController, :get_notification)
post("/media", MastodonAPIController, :upload)
put("/media/:id", MastodonAPIController, :update_media)
get("/lists", MastodonAPIController, :get_lists)
get("/lists/:id", MastodonAPIController, :get_list)
delete("/lists/:id", MastodonAPIController, :delete_list)
post("/lists", MastodonAPIController, :create_list)
put("/lists/:id", MastodonAPIController, :rename_list)
get("/lists/:id/accounts", MastodonAPIController, :list_accounts)
post("/lists/:id/accounts", MastodonAPIController, :add_to_list)
delete("/lists/:id/accounts", MastodonAPIController, :remove_from_list)
get("/domain_blocks", MastodonAPIController, :domain_blocks)
post("/domain_blocks", MastodonAPIController, :block_domain)
delete("/domain_blocks", MastodonAPIController, :unblock_domain)
get("/filters", MastodonAPIController, :get_filters)
post("/filters", MastodonAPIController, :create_filter)
get("/filters/:id", MastodonAPIController, :get_filter)
put("/filters/:id", MastodonAPIController, :update_filter)
delete("/filters/:id", MastodonAPIController, :delete_filter)
post("/push/subscription", MastodonAPIController, :create_push_subscription)
get("/push/subscription", MastodonAPIController, :get_push_subscription)
put("/push/subscription", MastodonAPIController, :update_push_subscription)
delete("/push/subscription", MastodonAPIController, :delete_push_subscription)
get("/suggestions", MastodonAPIController, :suggestions)
get("/endorsements", MastodonAPIController, :empty_array)
end
scope "/api/web", Pleroma.Web.MastodonAPI do
pipe_through(:authenticated_api)
put("/settings", MastodonAPIController, :put_settings)
end
scope "/api/v1", Pleroma.Web.MastodonAPI do
pipe_through(:api)
get("/instance", MastodonAPIController, :masto_instance)
get("/instance/peers", MastodonAPIController, :peers)
post("/apps", MastodonAPIController, :create_app)
get("/custom_emojis", MastodonAPIController, :custom_emojis)
get("/timelines/public", MastodonAPIController, :public_timeline)
get("/timelines/tag/:tag", MastodonAPIController, :hashtag_timeline)
get("/timelines/list/:list_id", MastodonAPIController, :list_timeline)
get("/statuses/:id", MastodonAPIController, :get_status)
get("/statuses/:id/context", MastodonAPIController, :get_context)
get("/statuses/:id/card", MastodonAPIController, :empty_object)
get("/statuses/:id/favourited_by", MastodonAPIController, :favourited_by)
get("/statuses/:id/reblogged_by", MastodonAPIController, :reblogged_by)
get("/accounts/:id/statuses", MastodonAPIController, :user_statuses)
get("/accounts/:id/followers", MastodonAPIController, :followers)
get("/accounts/:id/following", MastodonAPIController, :following)
get("/accounts/:id", MastodonAPIController, :user)
get("/trends", MastodonAPIController, :empty_array)
get("/search", MastodonAPIController, :search)
end
scope "/api/v2", Pleroma.Web.MastodonAPI do
pipe_through(:api)
get("/search", MastodonAPIController, :search2)
end
scope "/api", Pleroma.Web do
pipe_through(:config)
get("/help/test", TwitterAPI.UtilController, :help_test)
post("/help/test", TwitterAPI.UtilController, :help_test)
get("/statusnet/config", TwitterAPI.UtilController, :config)
get("/statusnet/version", TwitterAPI.UtilController, :version)
end
scope "/api", Pleroma.Web do
pipe_through(:api)
get("/statuses/user_timeline", TwitterAPI.Controller, :user_timeline)
get("/qvitter/statuses/user_timeline", TwitterAPI.Controller, :user_timeline)
get("/users/show", TwitterAPI.Controller, :show_user)
get("/statuses/followers", TwitterAPI.Controller, :followers)
get("/statuses/friends", TwitterAPI.Controller, :friends)
get("/statuses/show/:id", TwitterAPI.Controller, :fetch_status)
get("/statusnet/conversation/:id", TwitterAPI.Controller, :fetch_conversation)
post("/account/register", TwitterAPI.Controller, :register)
post("/account/password_reset", TwitterAPI.Controller, :password_reset)
- get("/account/confirm_email/:token", TwitterAPI.Controller, :confirm_email)
+
+ get("/account/confirm_email/:token", TwitterAPI.Controller, :confirm_email, as: :confirm_email)
get("/search", TwitterAPI.Controller, :search)
get("/statusnet/tags/timeline/:tag", TwitterAPI.Controller, :public_and_external_timeline)
end
scope "/api", Pleroma.Web do
pipe_through(:api)
get("/statuses/public_timeline", TwitterAPI.Controller, :public_timeline)
get(
"/statuses/public_and_external_timeline",
TwitterAPI.Controller,
:public_and_external_timeline
)
get("/statuses/networkpublic_timeline", TwitterAPI.Controller, :public_and_external_timeline)
end
scope "/api", Pleroma.Web, as: :twitter_api_search do
pipe_through(:api)
get("/pleroma/search_user", TwitterAPI.Controller, :search_user)
end
scope "/api", Pleroma.Web, as: :authenticated_twitter_api do
pipe_through(:authenticated_api)
get("/account/verify_credentials", TwitterAPI.Controller, :verify_credentials)
post("/account/verify_credentials", TwitterAPI.Controller, :verify_credentials)
post("/account/update_profile", TwitterAPI.Controller, :update_profile)
post("/account/update_profile_banner", TwitterAPI.Controller, :update_banner)
post("/qvitter/update_background_image", TwitterAPI.Controller, :update_background)
get("/statuses/home_timeline", TwitterAPI.Controller, :friends_timeline)
get("/statuses/friends_timeline", TwitterAPI.Controller, :friends_timeline)
get("/statuses/mentions", TwitterAPI.Controller, :mentions_timeline)
get("/statuses/mentions_timeline", TwitterAPI.Controller, :mentions_timeline)
get("/statuses/dm_timeline", TwitterAPI.Controller, :dm_timeline)
get("/qvitter/statuses/notifications", TwitterAPI.Controller, :notifications)
# XXX: this is really a pleroma API, but we want to keep the pleroma namespace clean
# for now.
post("/qvitter/statuses/notifications/read", TwitterAPI.Controller, :notifications_read)
post("/statuses/update", TwitterAPI.Controller, :status_update)
post("/statuses/retweet/:id", TwitterAPI.Controller, :retweet)
post("/statuses/unretweet/:id", TwitterAPI.Controller, :unretweet)
post("/statuses/destroy/:id", TwitterAPI.Controller, :delete_post)
get("/pleroma/friend_requests", TwitterAPI.Controller, :friend_requests)
post("/pleroma/friendships/approve", TwitterAPI.Controller, :approve_friend_request)
post("/pleroma/friendships/deny", TwitterAPI.Controller, :deny_friend_request)
post("/friendships/create", TwitterAPI.Controller, :follow)
post("/friendships/destroy", TwitterAPI.Controller, :unfollow)
post("/blocks/create", TwitterAPI.Controller, :block)
post("/blocks/destroy", TwitterAPI.Controller, :unblock)
post("/statusnet/media/upload", TwitterAPI.Controller, :upload)
post("/media/upload", TwitterAPI.Controller, :upload_json)
post("/media/metadata/create", TwitterAPI.Controller, :update_media)
post("/favorites/create/:id", TwitterAPI.Controller, :favorite)
post("/favorites/create", TwitterAPI.Controller, :favorite)
post("/favorites/destroy/:id", TwitterAPI.Controller, :unfavorite)
post("/qvitter/update_avatar", TwitterAPI.Controller, :update_avatar)
get("/friends/ids", TwitterAPI.Controller, :friends_ids)
get("/friendships/no_retweets/ids", TwitterAPI.Controller, :empty_array)
get("/mutes/users/ids", TwitterAPI.Controller, :empty_array)
get("/qvitter/mutes", TwitterAPI.Controller, :raw_empty_array)
get("/externalprofile/show", TwitterAPI.Controller, :external_profile)
end
pipeline :ap_relay do
plug(:accepts, ["activity+json"])
end
pipeline :ostatus do
plug(:accepts, ["xml", "atom", "html", "activity+json"])
end
scope "/", Pleroma.Web do
pipe_through(:ostatus)
get("/objects/:uuid", OStatus.OStatusController, :object)
get("/activities/:uuid", OStatus.OStatusController, :activity)
get("/notice/:id", OStatus.OStatusController, :notice)
get("/users/:nickname/feed", OStatus.OStatusController, :feed)
get("/users/:nickname", OStatus.OStatusController, :feed_redirect)
post("/users/:nickname/salmon", OStatus.OStatusController, :salmon_incoming)
post("/push/hub/:nickname", Websub.WebsubController, :websub_subscription_request)
get("/push/subscriptions/:id", Websub.WebsubController, :websub_subscription_confirmation)
post("/push/subscriptions/:id", Websub.WebsubController, :websub_incoming)
end
pipeline :activitypub do
plug(:accepts, ["activity+json"])
plug(Pleroma.Web.Plugs.HTTPSignaturePlug)
end
scope "/", Pleroma.Web.ActivityPub do
# XXX: not really ostatus
pipe_through(:ostatus)
get("/users/:nickname/followers", ActivityPubController, :followers)
get("/users/:nickname/following", ActivityPubController, :following)
get("/users/:nickname/outbox", ActivityPubController, :outbox)
end
scope "/relay", Pleroma.Web.ActivityPub do
pipe_through(:ap_relay)
get("/", ActivityPubController, :relay)
end
scope "/", Pleroma.Web.ActivityPub do
pipe_through(:activitypub)
post("/users/:nickname/inbox", ActivityPubController, :inbox)
post("/inbox", ActivityPubController, :inbox)
end
scope "/.well-known", Pleroma.Web do
pipe_through(:well_known)
get("/host-meta", WebFinger.WebFingerController, :host_meta)
get("/webfinger", WebFinger.WebFingerController, :webfinger)
get("/nodeinfo", Nodeinfo.NodeinfoController, :schemas)
end
scope "/nodeinfo", Pleroma.Web do
get("/:version", Nodeinfo.NodeinfoController, :nodeinfo)
end
scope "/", Pleroma.Web.MastodonAPI do
pipe_through(:mastodon_html)
get("/web/login", MastodonAPIController, :login)
post("/web/login", MastodonAPIController, :login_post)
get("/web/*path", MastodonAPIController, :index)
delete("/auth/sign_out", MastodonAPIController, :logout)
end
pipeline :remote_media do
end
scope "/proxy/", Pleroma.Web.MediaProxy do
pipe_through(:remote_media)
get("/:sig/:url", MediaProxyController, :remote)
get("/:sig/:url/:filename", MediaProxyController, :remote)
end
if Mix.env() == :dev do
scope "/dev" do
pipe_through([:mailbox_preview])
forward("/mailbox", Plug.Swoosh.MailboxPreview, base_path: "/dev/mailbox")
end
end
scope "/", Fallback do
get("/registration/:token", RedirectController, :registration_page)
get("/*path", RedirectController, :redirector)
options("/*path", RedirectController, :empty)
end
end
defmodule Fallback.RedirectController do
use Pleroma.Web, :controller
def redirector(conn, _params) do
conn
|> put_resp_content_type("text/html")
|> send_file(200, Pleroma.Plugs.InstanceStatic.file_path("index.html"))
end
def registration_page(conn, params) do
redirector(conn, params)
end
def empty(conn, _params) do
conn
|> put_status(204)
|> text("")
end
end
diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex
index 38653f0b8..3baeba619 100644
--- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex
+++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex
@@ -1,291 +1,293 @@
defmodule Pleroma.Web.TwitterAPI.UtilController do
use Pleroma.Web, :controller
require Logger
alias Pleroma.Web
alias Pleroma.Web.OStatus
alias Pleroma.Web.WebFinger
alias Pleroma.Web.CommonAPI
alias Comeonin.Pbkdf2
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.{Repo, PasswordResetToken, User, Emoji}
def show_password_reset(conn, %{"token" => token}) do
with %{used: false} = token <- Repo.get_by(PasswordResetToken, %{token: token}),
%User{} = user <- Repo.get(User, token.user_id) do
render(conn, "password_reset.html", %{
token: token,
user: user
})
else
_e -> render(conn, "invalid_token.html")
end
end
def password_reset(conn, %{"data" => data}) do
with {:ok, _} <- PasswordResetToken.reset_password(data["token"], data) do
render(conn, "password_reset_success.html")
else
_e -> render(conn, "password_reset_failed.html")
end
end
def help_test(conn, _params) do
json(conn, "ok")
end
def remote_subscribe(conn, %{"nickname" => nick, "profile" => _}) do
with %User{} = user <- User.get_cached_by_nickname(nick), avatar = User.avatar_url(user) do
conn
|> render("subscribe.html", %{nickname: nick, avatar: avatar, error: false})
else
_e ->
render(conn, "subscribe.html", %{
nickname: nick,
avatar: nil,
error: "Could not find user"
})
end
end
def remote_subscribe(conn, %{"user" => %{"nickname" => nick, "profile" => profile}}) do
with {:ok, %{"subscribe_address" => template}} <- WebFinger.finger(profile),
%User{ap_id: ap_id} <- User.get_cached_by_nickname(nick) do
conn
|> Phoenix.Controller.redirect(external: String.replace(template, "{uri}", ap_id))
else
_e ->
render(conn, "subscribe.html", %{
nickname: nick,
avatar: nil,
error: "Something went wrong."
})
end
end
def remote_follow(%{assigns: %{user: user}} = conn, %{"acct" => acct}) do
{err, followee} = OStatus.find_or_make_user(acct)
avatar = User.avatar_url(followee)
name = followee.nickname
id = followee.id
if !!user do
conn
|> render("follow.html", %{error: err, acct: acct, avatar: avatar, name: name, id: id})
else
conn
|> render("follow_login.html", %{
error: false,
acct: acct,
avatar: avatar,
name: name,
id: id
})
end
end
def do_remote_follow(conn, %{
"authorization" => %{"name" => username, "password" => password, "id" => id}
}) do
followee = Repo.get(User, id)
avatar = User.avatar_url(followee)
name = followee.nickname
with %User{} = user <- User.get_cached_by_nickname(username),
true <- Pbkdf2.checkpw(password, user.password_hash),
%User{} = _followed <- Repo.get(User, id),
{:ok, follower} <- User.follow(user, followee),
{:ok, _activity} <- ActivityPub.follow(follower, followee) do
conn
|> render("followed.html", %{error: false})
else
# Was already following user
{:error, "Could not follow user:" <> _rest} ->
render(conn, "followed.html", %{error: false})
_e ->
conn
|> render("follow_login.html", %{
error: "Wrong username or password",
id: id,
name: name,
avatar: avatar
})
end
end
def do_remote_follow(%{assigns: %{user: user}} = conn, %{"user" => %{"id" => id}}) do
with %User{} = followee <- Repo.get(User, id),
{:ok, follower} <- User.follow(user, followee),
{:ok, _activity} <- ActivityPub.follow(follower, followee) do
conn
|> render("followed.html", %{error: false})
else
# Was already following user
{:error, "Could not follow user:" <> _rest} ->
conn
|> render("followed.html", %{error: false})
e ->
Logger.debug("Remote follow failed with error #{inspect(e)}")
conn
|> render("followed.html", %{error: inspect(e)})
end
end
def config(conn, _params) do
instance = Pleroma.Config.get(:instance)
instance_fe = Pleroma.Config.get(:fe)
instance_chat = Pleroma.Config.get(:chat)
case get_format(conn) do
"xml" ->
response = """
<config>
<site>
<name>#{Keyword.get(instance, :name)}</name>
<site>#{Web.base_url()}</site>
<textlimit>#{Keyword.get(instance, :limit)}</textlimit>
<closed>#{!Keyword.get(instance, :registrations_open)}</closed>
</site>
</config>
"""
conn
|> put_resp_content_type("application/xml")
|> send_resp(200, response)
_ ->
vapid_public_key = Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key)
uploadlimit = %{
uploadlimit: to_string(Keyword.get(instance, :upload_limit)),
avatarlimit: to_string(Keyword.get(instance, :avatar_upload_limit)),
backgroundlimit: to_string(Keyword.get(instance, :background_upload_limit)),
bannerlimit: to_string(Keyword.get(instance, :banner_upload_limit))
}
data = %{
name: Keyword.get(instance, :name),
description: Keyword.get(instance, :description),
server: Web.base_url(),
textlimit: to_string(Keyword.get(instance, :limit)),
uploadlimit: uploadlimit,
closed: if(Keyword.get(instance, :registrations_open), do: "0", else: "1"),
private: if(Keyword.get(instance, :public, true), do: "0", else: "1"),
vapidPublicKey: vapid_public_key,
+ accountActivationRequired:
+ if(Keyword.get(instance, :account_activation_required, false), do: "1", else: "0"),
invitesEnabled: if(Keyword.get(instance, :invites_enabled, false), do: "1", else: "0")
}
pleroma_fe = %{
theme: Keyword.get(instance_fe, :theme),
background: Keyword.get(instance_fe, :background),
logo: Keyword.get(instance_fe, :logo),
logoMask: Keyword.get(instance_fe, :logo_mask),
logoMargin: Keyword.get(instance_fe, :logo_margin),
redirectRootNoLogin: Keyword.get(instance_fe, :redirect_root_no_login),
redirectRootLogin: Keyword.get(instance_fe, :redirect_root_login),
chatDisabled: !Keyword.get(instance_chat, :enabled),
showInstanceSpecificPanel: Keyword.get(instance_fe, :show_instance_panel),
scopeOptionsEnabled: Keyword.get(instance_fe, :scope_options_enabled),
formattingOptionsEnabled: Keyword.get(instance_fe, :formatting_options_enabled),
collapseMessageWithSubject: Keyword.get(instance_fe, :collapse_message_with_subject),
hidePostStats: Keyword.get(instance_fe, :hide_post_stats),
hideUserStats: Keyword.get(instance_fe, :hide_user_stats),
scopeCopy: Keyword.get(instance_fe, :scope_copy),
subjectLineBehavior: Keyword.get(instance_fe, :subject_line_behavior),
alwaysShowSubjectInput: Keyword.get(instance_fe, :always_show_subject_input)
}
managed_config = Keyword.get(instance, :managed_config)
data =
if managed_config do
data |> Map.put("pleromafe", pleroma_fe)
else
data
end
json(conn, %{site: data})
end
end
def version(conn, _params) do
version = Pleroma.Application.named_version()
case get_format(conn) do
"xml" ->
response = "<version>#{version}</version>"
conn
|> put_resp_content_type("application/xml")
|> send_resp(200, response)
_ ->
json(conn, version)
end
end
def emoji(conn, _params) do
json(conn, Enum.into(Emoji.get_all(), %{}))
end
def follow_import(conn, %{"list" => %Plug.Upload{} = listfile}) do
follow_import(conn, %{"list" => File.read!(listfile.path)})
end
def follow_import(%{assigns: %{user: user}} = conn, %{"list" => list}) do
Task.start(fn ->
String.split(list)
|> Enum.map(fn account ->
with %User{} = follower <- User.get_cached_by_ap_id(user.ap_id),
%User{} = followed <- User.get_or_fetch(account),
{:ok, follower} <- User.maybe_direct_follow(follower, followed) do
ActivityPub.follow(follower, followed)
else
err -> Logger.debug("follow_import: following #{account} failed with #{inspect(err)}")
end
end)
end)
json(conn, "job started")
end
def change_password(%{assigns: %{user: user}} = conn, params) do
case CommonAPI.Utils.confirm_current_password(user, params["password"]) do
{:ok, user} ->
with {:ok, _user} <-
User.reset_password(user, %{
password: params["new_password"],
password_confirmation: params["new_password_confirmation"]
}) do
json(conn, %{status: "success"})
else
{:error, changeset} ->
{_, {error, _}} = Enum.at(changeset.errors, 0)
json(conn, %{error: "New password #{error}."})
_ ->
json(conn, %{error: "Unable to change password."})
end
{:error, msg} ->
json(conn, %{error: msg})
end
end
def delete_account(%{assigns: %{user: user}} = conn, params) do
case CommonAPI.Utils.confirm_current_password(user, params["password"]) do
{:ok, user} ->
Task.start(fn -> User.delete(user) end)
json(conn, %{status: "success"})
{:error, msg} ->
json(conn, %{error: msg})
end
end
def captcha(conn, _params) do
json(conn, Pleroma.Captcha.new())
end
end
diff --git a/lib/pleroma/web/twitter_api/twitter_api.ex b/lib/pleroma/web/twitter_api/twitter_api.ex
index 90b8345c5..b77761aa4 100644
--- a/lib/pleroma/web/twitter_api/twitter_api.ex
+++ b/lib/pleroma/web/twitter_api/twitter_api.ex
@@ -1,317 +1,335 @@
defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
alias Pleroma.{UserInviteToken, User, Activity, Repo, Object}
+ alias Pleroma.{UserEmail, Mailer}
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.TwitterAPI.UserView
alias Pleroma.Web.CommonAPI
+
import Ecto.Query
def create_status(%User{} = user, %{"status" => _} = data) do
CommonAPI.post(user, data)
end
def delete(%User{} = user, id) do
with %Activity{data: %{"type" => _type}} <- Repo.get(Activity, id),
{:ok, activity} <- CommonAPI.delete(id, user) do
{:ok, activity}
end
end
def follow(%User{} = follower, params) do
with {:ok, %User{} = followed} <- get_user(params),
{:ok, follower} <- User.maybe_direct_follow(follower, followed),
{:ok, activity} <- ActivityPub.follow(follower, followed),
{:ok, follower, followed} <-
User.wait_and_refresh(
Pleroma.Config.get([:activitypub, :follow_handshake_timeout]),
follower,
followed
) do
{:ok, follower, followed, activity}
else
err -> err
end
end
def unfollow(%User{} = follower, params) do
with {:ok, %User{} = unfollowed} <- get_user(params),
{:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
{:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed) do
{:ok, follower, unfollowed}
else
err -> err
end
end
def block(%User{} = blocker, params) do
with {:ok, %User{} = blocked} <- get_user(params),
{:ok, blocker} <- User.block(blocker, blocked),
{:ok, _activity} <- ActivityPub.block(blocker, blocked) do
{:ok, blocker, blocked}
else
err -> err
end
end
def unblock(%User{} = blocker, params) do
with {:ok, %User{} = blocked} <- get_user(params),
{:ok, blocker} <- User.unblock(blocker, blocked),
{:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
{:ok, blocker, blocked}
else
err -> err
end
end
def repeat(%User{} = user, ap_id_or_id) do
with {:ok, _announce, %{data: %{"id" => id}}} <- CommonAPI.repeat(ap_id_or_id, user),
%Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
{:ok, activity}
end
end
def unrepeat(%User{} = user, ap_id_or_id) do
with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
%Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
{:ok, activity}
end
end
def fav(%User{} = user, ap_id_or_id) do
with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
%Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
{:ok, activity}
end
end
def unfav(%User{} = user, ap_id_or_id) do
with {:ok, _unfav, _fav, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
%Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
{:ok, activity}
end
end
def upload(%Plug.Upload{} = file, %User{} = user, format \\ "xml") do
{:ok, object} = ActivityPub.upload(file, actor: User.ap_id(user))
url = List.first(object.data["url"])
href = url["href"]
type = url["mediaType"]
case format do
"xml" ->
# Fake this as good as possible...
"""
<?xml version="1.0" encoding="UTF-8"?>
<rsp stat="ok" xmlns:atom="http://www.w3.org/2005/Atom">
<mediaid>#{object.id}</mediaid>
<media_id>#{object.id}</media_id>
<media_id_string>#{object.id}</media_id_string>
<media_url>#{href}</media_url>
<mediaurl>#{href}</mediaurl>
<atom:link rel="enclosure" href="#{href}" type="#{type}"></atom:link>
</rsp>
"""
"json" ->
%{
media_id: object.id,
media_id_string: "#{object.id}}",
media_url: href,
size: 0
}
|> Jason.encode!()
end
end
def register_user(params) do
tokenString = params["token"]
params = %{
nickname: params["nickname"],
name: params["fullname"],
bio: User.parse_bio(params["bio"]),
email: params["email"],
password: params["password"],
password_confirmation: params["confirm"],
captcha_solution: params["captcha_solution"],
captcha_token: params["captcha_token"]
}
captcha_enabled = Pleroma.Config.get([Pleroma.Captcha, :enabled])
# true if captcha is disabled or enabled and valid, false otherwise
captcha_ok =
if !captcha_enabled do
true
else
Pleroma.Captcha.validate(params[:captcha_token], params[:captcha_solution])
end
# Captcha invalid
if not captcha_ok do
# I have no idea how this error handling works
{:error, %{error: Jason.encode!(%{captcha: ["Invalid CAPTCHA"]})}}
else
registrations_open = Pleroma.Config.get([:instance, :registrations_open])
# no need to query DB if registration is open
token =
unless registrations_open || is_nil(tokenString) do
Repo.get_by(UserInviteToken, %{token: tokenString})
end
cond do
registrations_open || (!is_nil(token) && !token.used) ->
changeset = User.register_changeset(%User{info: %{}}, params)
with {:ok, user} <- Repo.insert(changeset) do
!registrations_open && UserInviteToken.mark_as_used(token.token)
+
+ if Pleroma.Config.get([:instance, :account_activation_required]) do
+ info_change = User.Info.confirmation_update(user.info, :unconfirmed)
+
+ {:ok, unconfirmed_user} =
+ user
+ |> Ecto.Changeset.change()
+ |> Ecto.Changeset.put_embed(:info, info_change)
+ |> Repo.update()
+
+ {:ok, _} =
+ unconfirmed_user
+ |> UserEmail.account_confirmation_email()
+ |> Mailer.deliver()
+ end
+
{:ok, user}
else
{:error, changeset} ->
errors =
Ecto.Changeset.traverse_errors(changeset, fn {msg, _opts} -> msg end)
|> Jason.encode!()
{:error, %{error: errors}}
end
!registrations_open && is_nil(token) ->
{:error, "Invalid token"}
!registrations_open && token.used ->
{:error, "Expired token"}
end
end
end
def password_reset(nickname_or_email) do
with true <- is_binary(nickname_or_email),
%User{local: true} = user <- User.get_by_nickname_or_email(nickname_or_email),
{:ok, token_record} <- Pleroma.PasswordResetToken.create_token(user) do
user
- |> Pleroma.UserEmail.password_reset_email(token_record.token)
- |> Pleroma.Mailer.deliver()
+ |> UserEmail.password_reset_email(token_record.token)
+ |> Mailer.deliver()
else
false ->
{:error, "bad user identifier"}
%User{local: false} ->
{:error, "remote user"}
nil ->
{:error, "unknown user"}
end
end
def get_by_id_or_nickname(id_or_nickname) do
if !is_integer(id_or_nickname) && :error == Integer.parse(id_or_nickname) do
Repo.get_by(User, nickname: id_or_nickname)
else
Repo.get(User, id_or_nickname)
end
end
def get_user(user \\ nil, params) do
case params do
%{"user_id" => user_id} ->
case target = get_by_id_or_nickname(user_id) do
nil ->
{:error, "No user with such user_id"}
_ ->
{:ok, target}
end
%{"screen_name" => nickname} ->
case target = Repo.get_by(User, nickname: nickname) do
nil ->
{:error, "No user with such screen_name"}
_ ->
{:ok, target}
end
_ ->
if user do
{:ok, user}
else
{:error, "You need to specify screen_name or user_id"}
end
end
end
defp parse_int(string, default)
defp parse_int(string, default) when is_binary(string) do
with {n, _} <- Integer.parse(string) do
n
else
_e -> default
end
end
defp parse_int(_, default), do: default
def search(_user, %{"q" => query} = params) do
limit = parse_int(params["rpp"], 20)
page = parse_int(params["page"], 1)
offset = (page - 1) * limit
q =
from(
a in Activity,
where: fragment("?->>'type' = 'Create'", a.data),
where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
where:
fragment(
"to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)",
a.data,
^query
),
limit: ^limit,
offset: ^offset,
# this one isn't indexed so psql won't take the wrong index.
order_by: [desc: :inserted_at]
)
_activities = Repo.all(q)
end
# DEPRECATED mostly, context objects are now created at insertion time.
def context_to_conversation_id(context) do
with %Object{id: id} <- Object.get_cached_by_ap_id(context) do
id
else
_e ->
changeset = Object.context_mapping(context)
case Repo.insert(changeset) do
{:ok, %{id: id}} ->
id
# This should be solved by an upsert, but it seems ecto
# has problems accessing the constraint inside the jsonb.
{:error, _} ->
Object.get_cached_by_ap_id(context).id
end
end
end
def conversation_id_to_context(id) do
with %Object{data: %{"id" => context}} <- Repo.get(Object, id) do
context
else
_e ->
{:error, "No such conversation"}
end
end
def get_external_profile(for_user, uri) do
with %User{} = user <- User.get_or_fetch(uri) do
{:ok, UserView.render("show.json", %{user: user, for: for_user})}
else
_e ->
{:error, "Couldn't find user"}
end
end
end
diff --git a/lib/pleroma/web/twitter_api/twitter_api_controller.ex b/lib/pleroma/web/twitter_api/twitter_api_controller.ex
index 2680be25f..e8a3150e9 100644
--- a/lib/pleroma/web/twitter_api/twitter_api_controller.ex
+++ b/lib/pleroma/web/twitter_api/twitter_api_controller.ex
@@ -1,659 +1,659 @@
defmodule Pleroma.Web.TwitterAPI.Controller do
use Pleroma.Web, :controller
import Pleroma.Web.ControllerHelper, only: [json_response: 3]
alias Pleroma.Web.TwitterAPI.{TwitterAPI, UserView, ActivityView, NotificationView}
alias Pleroma.Web.CommonAPI
alias Pleroma.{Repo, Activity, Object, User, Notification}
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils
alias Ecto.Changeset
require Logger
plug(:only_if_public_instance when action in [:public_timeline, :public_and_external_timeline])
+ plug(:fetch_flash when action in [:confirm_email])
action_fallback(:errors)
def verify_credentials(%{assigns: %{user: user}} = conn, _params) do
token = Phoenix.Token.sign(conn, "user socket", user.id)
conn
|> put_view(UserView)
|> render("show.json", %{user: user, token: token})
end
def status_update(%{assigns: %{user: user}} = conn, %{"status" => _} = status_data) do
with media_ids <- extract_media_ids(status_data),
{:ok, activity} <-
TwitterAPI.create_status(user, Map.put(status_data, "media_ids", media_ids)) do
conn
|> json(ActivityView.render("activity.json", activity: activity, for: user))
else
_ -> empty_status_reply(conn)
end
end
def status_update(conn, _status_data) do
empty_status_reply(conn)
end
defp empty_status_reply(conn) do
bad_request_reply(conn, "Client must provide a 'status' parameter with a value.")
end
defp extract_media_ids(status_data) do
with media_ids when not is_nil(media_ids) <- status_data["media_ids"],
split_ids <- String.split(media_ids, ","),
clean_ids <- Enum.reject(split_ids, fn id -> String.length(id) == 0 end) do
clean_ids
else
_e -> []
end
end
def public_and_external_timeline(%{assigns: %{user: user}} = conn, params) do
params =
params
|> Map.put("type", ["Create", "Announce"])
|> Map.put("blocking_user", user)
activities = ActivityPub.fetch_public_activities(params)
conn
|> put_view(ActivityView)
|> render("index.json", %{activities: activities, for: user})
end
def public_timeline(%{assigns: %{user: user}} = conn, params) do
params =
params
|> Map.put("type", ["Create", "Announce"])
|> Map.put("local_only", true)
|> Map.put("blocking_user", user)
activities = ActivityPub.fetch_public_activities(params)
conn
|> put_view(ActivityView)
|> render("index.json", %{activities: activities, for: user})
end
def friends_timeline(%{assigns: %{user: user}} = conn, params) do
params =
params
|> Map.put("type", ["Create", "Announce", "Follow", "Like"])
|> Map.put("blocking_user", user)
|> Map.put("user", user)
activities =
ActivityPub.fetch_activities([user.ap_id | user.following], params)
|> ActivityPub.contain_timeline(user)
conn
|> put_view(ActivityView)
|> render("index.json", %{activities: activities, for: user})
end
def show_user(conn, params) do
with {:ok, shown} <- TwitterAPI.get_user(params) do
params =
if user = conn.assigns.user do
%{user: shown, for: user}
else
%{user: shown}
end
conn
|> put_view(UserView)
|> render("show.json", params)
else
{:error, msg} ->
bad_request_reply(conn, msg)
end
end
def user_timeline(%{assigns: %{user: user}} = conn, params) do
case TwitterAPI.get_user(user, params) do
{:ok, target_user} ->
activities = ActivityPub.fetch_user_activities(target_user, user, params)
conn
|> put_view(ActivityView)
|> render("index.json", %{activities: activities, for: user})
{:error, msg} ->
bad_request_reply(conn, msg)
end
end
def mentions_timeline(%{assigns: %{user: user}} = conn, params) do
params =
params
|> Map.put("type", ["Create", "Announce", "Follow", "Like"])
|> Map.put("blocking_user", user)
activities = ActivityPub.fetch_activities([user.ap_id], params)
conn
|> put_view(ActivityView)
|> render("index.json", %{activities: activities, for: user})
end
def dm_timeline(%{assigns: %{user: user}} = conn, params) do
query =
ActivityPub.fetch_activities_query(
[user.ap_id],
Map.merge(params, %{"type" => "Create", "user" => user, visibility: "direct"})
)
activities = Repo.all(query)
conn
|> put_view(ActivityView)
|> render("index.json", %{activities: activities, for: user})
end
def notifications(%{assigns: %{user: user}} = conn, params) do
notifications = Notification.for_user(user, params)
conn
|> put_view(NotificationView)
|> render("notification.json", %{notifications: notifications, for: user})
end
def notifications_read(%{assigns: %{user: user}} = conn, %{"latest_id" => latest_id} = params) do
Notification.set_read_up_to(user, latest_id)
notifications = Notification.for_user(user, params)
conn
|> put_view(NotificationView)
|> render("notification.json", %{notifications: notifications, for: user})
end
def notifications_read(%{assigns: %{user: _user}} = conn, _) do
bad_request_reply(conn, "You need to specify latest_id")
end
def follow(%{assigns: %{user: user}} = conn, params) do
case TwitterAPI.follow(user, params) do
{:ok, user, followed, _activity} ->
conn
|> put_view(UserView)
|> render("show.json", %{user: followed, for: user})
{:error, msg} ->
forbidden_json_reply(conn, msg)
end
end
def block(%{assigns: %{user: user}} = conn, params) do
case TwitterAPI.block(user, params) do
{:ok, user, blocked} ->
conn
|> put_view(UserView)
|> render("show.json", %{user: blocked, for: user})
{:error, msg} ->
forbidden_json_reply(conn, msg)
end
end
def unblock(%{assigns: %{user: user}} = conn, params) do
case TwitterAPI.unblock(user, params) do
{:ok, user, blocked} ->
conn
|> put_view(UserView)
|> render("show.json", %{user: blocked, for: user})
{:error, msg} ->
forbidden_json_reply(conn, msg)
end
end
def delete_post(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with {:ok, activity} <- TwitterAPI.delete(user, id) do
conn
|> put_view(ActivityView)
|> render("activity.json", %{activity: activity, for: user})
end
end
def unfollow(%{assigns: %{user: user}} = conn, params) do
case TwitterAPI.unfollow(user, params) do
{:ok, user, unfollowed} ->
conn
|> put_view(UserView)
|> render("show.json", %{user: unfollowed, for: user})
{:error, msg} ->
forbidden_json_reply(conn, msg)
end
end
def fetch_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Activity{} = activity <- Repo.get(Activity, id),
true <- ActivityPub.visible_for_user?(activity, user) do
conn
|> put_view(ActivityView)
|> render("activity.json", %{activity: activity, for: user})
end
end
def fetch_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
id = String.to_integer(id)
with context when is_binary(context) <- TwitterAPI.conversation_id_to_context(id),
activities <-
ActivityPub.fetch_activities_for_context(context, %{
"blocking_user" => user,
"user" => user
}) do
conn
|> put_view(ActivityView)
|> render("index.json", %{activities: activities, for: user})
end
end
@doc """
Updates metadata of uploaded media object.
Derived from [Twitter API endpoint](https://developer.twitter.com/en/docs/media/upload-media/api-reference/post-media-metadata-create).
"""
def update_media(%{assigns: %{user: user}} = conn, %{"media_id" => id} = data) do
object = Repo.get(Object, id)
description = get_in(data, ["alt_text", "text"]) || data["name"] || data["description"]
{conn, status, response_body} =
cond do
!object ->
{halt(conn), :not_found, ""}
!Object.authorize_mutation(object, user) ->
{halt(conn), :forbidden, "You can only update your own uploads."}
!is_binary(description) ->
{conn, :not_modified, ""}
true ->
new_data = Map.put(object.data, "name", description)
{:ok, _} =
object
|> Object.change(%{data: new_data})
|> Repo.update()
{conn, :no_content, ""}
end
conn
|> put_status(status)
|> json(response_body)
end
def upload(%{assigns: %{user: user}} = conn, %{"media" => media}) do
response = TwitterAPI.upload(media, user)
conn
|> put_resp_content_type("application/atom+xml")
|> send_resp(200, response)
end
def upload_json(%{assigns: %{user: user}} = conn, %{"media" => media}) do
response = TwitterAPI.upload(media, user, "json")
conn
|> json_reply(200, response)
end
def get_by_id_or_ap_id(id) do
activity = Repo.get(Activity, id) || Activity.get_create_activity_by_object_ap_id(id)
if activity.data["type"] == "Create" do
activity
else
Activity.get_create_activity_by_object_ap_id(activity.data["object"])
end
end
def favorite(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with {_, {:ok, id}} <- {:param_cast, Ecto.Type.cast(:integer, id)},
{:ok, activity} <- TwitterAPI.fav(user, id) do
conn
|> put_view(ActivityView)
|> render("activity.json", %{activity: activity, for: user})
end
end
def unfavorite(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with {_, {:ok, id}} <- {:param_cast, Ecto.Type.cast(:integer, id)},
{:ok, activity} <- TwitterAPI.unfav(user, id) do
conn
|> put_view(ActivityView)
|> render("activity.json", %{activity: activity, for: user})
end
end
def retweet(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with {_, {:ok, id}} <- {:param_cast, Ecto.Type.cast(:integer, id)},
{:ok, activity} <- TwitterAPI.repeat(user, id) do
conn
|> put_view(ActivityView)
|> render("activity.json", %{activity: activity, for: user})
end
end
def unretweet(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with {_, {:ok, id}} <- {:param_cast, Ecto.Type.cast(:integer, id)},
{:ok, activity} <- TwitterAPI.unrepeat(user, id) do
conn
|> put_view(ActivityView)
|> render("activity.json", %{activity: activity, for: user})
end
end
def register(conn, params) do
with {:ok, user} <- TwitterAPI.register_user(params) do
conn
|> put_view(UserView)
|> render("show.json", %{user: user})
else
{:error, errors} ->
conn
|> json_reply(400, Jason.encode!(errors))
end
end
def password_reset(conn, params) do
nickname_or_email = params["email"] || params["nickname"]
with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
json_response(conn, :no_content, "")
end
end
def confirm_email(conn, %{"token" => token}) do
with %User{} = user <- User.get_by_confirmation_token(token),
true <- user.local,
- new_info_fields <- %{confirmation_pending: false, confirmation_token: nil},
- info_change <- User.Info.confirmation_update(user.info, new_info_fields),
+ info_change <- User.Info.confirmation_update(user.info, :confirmed),
changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_change),
{:ok, _} <- User.update_and_set_cache(changeset) do
conn
|> put_flash(:info, "Email confirmed. Please sign in.")
|> redirect(to: "/")
end
end
def update_avatar(%{assigns: %{user: user}} = conn, params) do
{:ok, object} = ActivityPub.upload(params, type: :avatar)
change = Changeset.change(user, %{avatar: object.data})
{:ok, user} = User.update_and_set_cache(change)
CommonAPI.update(user)
conn
|> put_view(UserView)
|> render("show.json", %{user: user, for: user})
end
def update_banner(%{assigns: %{user: user}} = conn, params) do
with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
new_info <- %{"banner" => object.data},
info_cng <- User.Info.profile_update(user.info, new_info),
changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
{:ok, user} <- User.update_and_set_cache(changeset) do
CommonAPI.update(user)
%{"url" => [%{"href" => href} | _]} = object.data
response = %{url: href} |> Jason.encode!()
conn
|> json_reply(200, response)
end
end
def update_background(%{assigns: %{user: user}} = conn, params) do
with {:ok, object} <- ActivityPub.upload(params, type: :background),
new_info <- %{"background" => object.data},
info_cng <- User.Info.profile_update(user.info, new_info),
changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
{:ok, _user} <- User.update_and_set_cache(changeset) do
%{"url" => [%{"href" => href} | _]} = object.data
response = %{url: href} |> Jason.encode!()
conn
|> json_reply(200, response)
end
end
def external_profile(%{assigns: %{user: current_user}} = conn, %{"profileurl" => uri}) do
with {:ok, user_map} <- TwitterAPI.get_external_profile(current_user, uri),
response <- Jason.encode!(user_map) do
conn
|> json_reply(200, response)
else
_e ->
conn
|> put_status(404)
|> json(%{error: "Can't find user"})
end
end
def followers(%{assigns: %{user: for_user}} = conn, params) do
with {:ok, user} <- TwitterAPI.get_user(for_user, params),
{:ok, followers} <- User.get_followers(user) do
followers =
cond do
for_user && user.id == for_user.id -> followers
user.info.hide_network -> []
true -> followers
end
conn
|> put_view(UserView)
|> render("index.json", %{users: followers, for: conn.assigns[:user]})
else
_e -> bad_request_reply(conn, "Can't get followers")
end
end
def friends(%{assigns: %{user: for_user}} = conn, params) do
with {:ok, user} <- TwitterAPI.get_user(conn.assigns[:user], params),
{:ok, friends} <- User.get_friends(user) do
friends =
cond do
for_user && user.id == for_user.id -> friends
user.info.hide_network -> []
true -> friends
end
conn
|> put_view(UserView)
|> render("index.json", %{users: friends, for: conn.assigns[:user]})
else
_e -> bad_request_reply(conn, "Can't get friends")
end
end
def friend_requests(conn, params) do
with {:ok, user} <- TwitterAPI.get_user(conn.assigns[:user], params),
{:ok, friend_requests} <- User.get_follow_requests(user) do
conn
|> put_view(UserView)
|> render("index.json", %{users: friend_requests, for: conn.assigns[:user]})
else
_e -> bad_request_reply(conn, "Can't get friend requests")
end
end
def approve_friend_request(conn, %{"user_id" => uid} = _params) do
with followed <- conn.assigns[:user],
uid when is_number(uid) <- String.to_integer(uid),
%User{} = follower <- Repo.get(User, uid),
{:ok, follower} <- User.maybe_follow(follower, followed),
%Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
{:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
{:ok, _activity} <-
ActivityPub.accept(%{
to: [follower.ap_id],
actor: followed.ap_id,
object: follow_activity.data["id"],
type: "Accept"
}) do
conn
|> put_view(UserView)
|> render("show.json", %{user: follower, for: followed})
else
e -> bad_request_reply(conn, "Can't approve user: #{inspect(e)}")
end
end
def deny_friend_request(conn, %{"user_id" => uid} = _params) do
with followed <- conn.assigns[:user],
uid when is_number(uid) <- String.to_integer(uid),
%User{} = follower <- Repo.get(User, uid),
%Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
{:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),
{:ok, _activity} <-
ActivityPub.reject(%{
to: [follower.ap_id],
actor: followed.ap_id,
object: follow_activity.data["id"],
type: "Reject"
}) do
conn
|> put_view(UserView)
|> render("show.json", %{user: follower, for: followed})
else
e -> bad_request_reply(conn, "Can't deny user: #{inspect(e)}")
end
end
def friends_ids(%{assigns: %{user: user}} = conn, _params) do
with {:ok, friends} <- User.get_friends(user) do
ids =
friends
|> Enum.map(fn x -> x.id end)
|> Jason.encode!()
json(conn, ids)
else
_e -> bad_request_reply(conn, "Can't get friends")
end
end
def empty_array(conn, _params) do
json(conn, Jason.encode!([]))
end
def raw_empty_array(conn, _params) do
json(conn, [])
end
defp build_info_cng(user, params) do
info_params =
["no_rich_text", "locked", "hide_network"]
|> Enum.reduce(%{}, fn key, res ->
if value = params[key] do
Map.put(res, key, value == "true")
else
res
end
end)
info_params =
if value = params["default_scope"] do
Map.put(info_params, "default_scope", value)
else
info_params
end
User.Info.profile_update(user.info, info_params)
end
defp parse_profile_bio(user, params) do
if bio = params["description"] do
Map.put(params, "bio", User.parse_bio(bio, user))
else
params
end
end
def update_profile(%{assigns: %{user: user}} = conn, params) do
params = parse_profile_bio(user, params)
info_cng = build_info_cng(user, params)
with changeset <- User.update_changeset(user, params),
changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
{:ok, user} <- User.update_and_set_cache(changeset) do
CommonAPI.update(user)
conn
|> put_view(UserView)
|> render("user.json", %{user: user, for: user})
else
error ->
Logger.debug("Can't update user: #{inspect(error)}")
bad_request_reply(conn, "Can't update user")
end
end
def search(%{assigns: %{user: user}} = conn, %{"q" => _query} = params) do
activities = TwitterAPI.search(user, params)
conn
|> put_view(ActivityView)
|> render("index.json", %{activities: activities, for: user})
end
def search_user(%{assigns: %{user: user}} = conn, %{"query" => query}) do
users = User.search(query, true)
conn
|> put_view(UserView)
|> render("index.json", %{users: users, for: user})
end
defp bad_request_reply(conn, error_message) do
json = error_json(conn, error_message)
json_reply(conn, 400, json)
end
defp json_reply(conn, status, json) do
conn
|> put_resp_content_type("application/json")
|> send_resp(status, json)
end
defp forbidden_json_reply(conn, error_message) do
json = error_json(conn, error_message)
json_reply(conn, 403, json)
end
def only_if_public_instance(conn = %{conn: %{assigns: %{user: _user}}}, _), do: conn
def only_if_public_instance(conn, _) do
if Keyword.get(Application.get_env(:pleroma, :instance), :public) do
conn
else
conn
|> forbidden_json_reply("Invalid credentials.")
|> halt()
end
end
defp error_json(conn, error_message) do
%{"error" => error_message, "request" => conn.request_path} |> Jason.encode!()
end
def errors(conn, {:param_cast, _}) do
conn
|> put_status(400)
|> json("Invalid parameters")
end
def errors(conn, _) do
conn
|> put_status(500)
|> json("Something went wrong")
end
end

File Metadata

Mime Type
text/x-diff
Expires
Sat, Oct 31, 5:33 AM (14 h, 7 s)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1801082
Default Alt Text
(96 KB)

Event Timeline