Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85710493
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
207 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/lib/mix/tasks/set_locked.ex b/lib/mix/tasks/set_locked.ex
new file mode 100644
index 000000000..2b3b18b09
--- /dev/null
+++ b/lib/mix/tasks/set_locked.ex
@@ -0,0 +1,30 @@
+defmodule Mix.Tasks.SetLocked do
+ use Mix.Task
+ import Mix.Ecto
+ alias Pleroma.{Repo, User}
+
+ @shortdoc "Set locked status"
+ def run([nickname | rest]) do
+ ensure_started(Repo, [])
+
+ locked =
+ case rest do
+ [locked] -> locked == "true"
+ _ -> true
+ end
+
+ with %User{local: true} = user <- User.get_by_nickname(nickname) do
+ info =
+ user.info
+ |> Map.put("locked", !!locked)
+
+ cng = User.info_changeset(user, %{info: info})
+ user = Repo.update!(cng)
+
+ IO.puts("locked status of #{nickname}: #{user.info["locked"]}")
+ else
+ _ ->
+ IO.puts("No local user #{nickname}")
+ end
+ end
+end
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index 00cac153d..b27397e13 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -1,617 +1,663 @@
defmodule Pleroma.User do
use Ecto.Schema
import Ecto.{Changeset, Query}
alias Pleroma.{Repo, User, Object, Web, Activity, Notification}
alias Comeonin.Pbkdf2
alias Pleroma.Web.{OStatus, Websub}
alias Pleroma.Web.ActivityPub.{Utils, ActivityPub}
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(:info, :map, default: %{})
field(:follower_address, :string)
field(:search_distance, :float, virtual: true)
has_many(:notifications, Notification)
timestamps()
end
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 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 info_changeset(struct, params \\ %{}) do
struct
|> cast(params, [:info])
|> validate_required([:info])
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"] || 0,
follower_count: user.info["follower_count"] || 0,
locked: user.info["locked"] || false
}
end
@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])?)*$/
def remote_user_creation(params) do
changes =
%User{}
|> cast(params, [:bio, :name, :ap_id, :nickname, :info, :avatar])
|> validate_required([:name, :ap_id, :nickname])
|> unique_constraint(:nickname)
|> validate_format(:nickname, @email_regex)
|> validate_length(:bio, max: 5000)
|> validate_length(:name, max: 100)
|> put_change(:local, false)
if changes.valid? do
case changes.changes[:info]["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])
|> unique_constraint(:nickname)
|> validate_format(:nickname, ~r/^[a-zA-Z\d]+$/)
|> validate_length(:bio, max: 5000)
|> validate_length(:name, min: 1, max: 100)
end
def upgrade_changeset(struct, params \\ %{}) do
struct
|> cast(params, [:bio, :name, :info, :follower_address, :avatar])
|> unique_constraint(:nickname)
|> validate_format(:nickname, ~r/^[a-zA-Z\d]+$/)
|> validate_length(:bio, max: 5000)
|> validate_length(:name, max: 100)
end
def password_update_changeset(struct, params) do
changeset =
struct
|> cast(params, [:password, :password_confirmation])
|> validate_required([:password, :password_confirmation])
|> validate_confirmation(:password)
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, ~r/^[a-zA-Z\d]+$/)
|> validate_format(:email, @email_regex)
|> validate_length(:bio, max: 1000)
|> validate_length(:name, min: 1, max: 100)
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 maybe_direct_follow(%User{} = follower, %User{info: info} = followed) do
user_info = user_info(followed)
should_direct_follow =
cond do
# if the account is locked, don't pre-create the relationship
user_info[:locked] == true ->
false
# if the users are blocking each other, we shouldn't even be here, but check for it anyway
User.blocks?(follower, followed) == true or User.blocks?(followed, follower) == true ->
false
# if OStatus, then there is no three-way handshake to follow
User.ap_enabled?(followed) != true ->
true
# if there are no other reasons not to, just pre-create the relationship
true ->
true
end
if should_direct_follow 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
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."}
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
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}")
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_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)
+
+ {:ok, users}
+ end
+
def increase_note_count(%User{} = user) do
note_count = (user.info["note_count"] || 0) + 1
new_info = Map.put(user.info, "note_count", note_count)
cs = info_changeset(user, %{info: new_info})
update_and_set_cache(cs)
end
def decrease_note_count(%User{} = user) do
note_count = user.info["note_count"] || 0
note_count = if note_count <= 0, do: 0, else: note_count - 1
new_info = Map.put(user.info, "note_count", note_count)
cs = info_changeset(user, %{info: new_info})
update_and_set_cache(cs)
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)
new_info = Map.put(user.info, "note_count", note_count)
cs = info_changeset(user, %{info: new_info})
update_and_set_cache(cs)
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)
new_info = Map.put(user.info, "follower_count", follower_count)
cs = info_changeset(user, %{info: new_info})
update_and_set_cache(cs)
end
def get_notified_from_activity(%Activity{recipients: to}) do
query =
from(
u in User,
where: u.ap_id in ^to,
where: u.local == true
)
Repo.all(query)
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) 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
)
}
)
q =
from(
s in subquery(inner),
order_by: s.search_distance,
limit: 20
)
Repo.all(q)
end
def block(user, %{ap_id: ap_id}) do
blocks = user.info["blocks"] || []
new_blocks = Enum.uniq([ap_id | blocks])
new_info = Map.put(user.info, "blocks", new_blocks)
cs = User.info_changeset(user, %{info: new_info})
update_and_set_cache(cs)
end
def unblock(user, %{ap_id: ap_id}) do
blocks = user.info["blocks"] || []
new_blocks = List.delete(blocks, ap_id)
new_info = Map.put(user.info, "blocks", new_blocks)
cs = User.info_changeset(user, %{info: new_info})
update_and_set_cache(cs)
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
domain_blocks = user.info["domain_blocks"] || []
new_blocks = Enum.uniq([domain | domain_blocks])
new_info = Map.put(user.info, "domain_blocks", new_blocks)
cs = User.info_changeset(user, %{info: new_info})
update_and_set_cache(cs)
end
def unblock_domain(user, domain) do
blocks = user.info["domain_blocks"] || []
new_blocks = List.delete(blocks, domain)
new_info = Map.put(user.info, "domain_blocks", new_blocks)
cs = User.info_changeset(user, %{info: new_info})
update_and_set_cache(cs)
end
def local_user_query() do
from(u in User, where: u.local == true)
end
def deactivate(%User{} = user) do
new_info = Map.put(user.info, "deactivated", true)
cs = User.info_changeset(user, %{info: new_info})
update_and_set_cache(cs)
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.get_by_ap_id(activity.data["object"]["id"]))
# TODO: Do something with likes, follows, repeats.
_ ->
"Doing nothing"
end
end)
:ok
end
def get_or_fetch_by_ap_id(ap_id) do
if user = get_by_ap_id(ap_id) 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
# AP style
def public_key_from_info(%{
"source_data" => %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
}) do
key =
:public_key.pem_decode(public_key_pem)
|> 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{info: info}), do: info["ap_enabled"]
def ap_enabled?(_), do: false
def get_or_fetch(uri_or_nickname) do
if String.starts_with?(uri_or_nickname, "http") do
get_or_fetch_by_ap_id(uri_or_nickname)
else
get_or_fetch_by_nickname(uri_or_nickname)
end
end
end
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index 43e96fe37..3c2875548 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -1,683 +1,684 @@
defmodule Pleroma.Web.ActivityPub.ActivityPub do
alias Pleroma.{Activity, Repo, Object, Upload, User, Notification}
alias Pleroma.Web.ActivityPub.{Transmogrifier, MRF}
alias Pleroma.Web.WebFinger
alias Pleroma.Web.Federator
alias Pleroma.Web.OStatus
import Ecto.Query
import Pleroma.Web.ActivityPub.Utils
require Logger
@httpoison Application.get_env(:pleroma, :httpoison)
@instance Application.get_env(:pleroma, :instance)
def get_recipients(data) do
(data["to"] || []) ++ (data["cc"] || [])
end
defp check_actor_is_active(actor) do
if not is_nil(actor) do
with user <- User.get_cached_by_ap_id(actor),
nil <- user.info["deactivated"] do
:ok
else
_e -> :reject
end
else
:ok
end
end
def insert(map, local \\ true) when is_map(map) do
with nil <- Activity.get_by_ap_id(map["id"]),
map <- lazy_put_activity_defaults(map),
:ok <- check_actor_is_active(map["actor"]),
{:ok, map} <- MRF.filter(map),
:ok <- insert_full_object(map) do
{:ok, activity} =
Repo.insert(%Activity{
data: map,
local: local,
actor: map["actor"],
recipients: get_recipients(map)
})
Notification.create_notifications(activity)
stream_out(activity)
{:ok, activity}
else
%Activity{} = activity -> {:ok, activity}
error -> {:error, error}
end
end
def stream_out(activity) do
public = "https://www.w3.org/ns/activitystreams#Public"
if activity.data["type"] in ["Create", "Announce"] do
Pleroma.Web.Streamer.stream("user", activity)
if Enum.member?(activity.data["to"], public) do
Pleroma.Web.Streamer.stream("public", activity)
if activity.local do
Pleroma.Web.Streamer.stream("public:local", activity)
end
else
if !Enum.member?(activity.data["cc"] || [], public) &&
!Enum.member?(
activity.data["to"],
User.get_by_ap_id(activity.data["actor"]).follower_address
),
do: Pleroma.Web.Streamer.stream("direct", activity)
end
end
end
def create(%{to: to, actor: actor, context: context, object: object} = params) do
additional = params[:additional] || %{}
# only accept false as false value
local = !(params[:local] == false)
published = params[:published]
with create_data <-
make_create_data(
%{to: to, actor: actor, published: published, context: context, object: object},
additional
),
{:ok, activity} <- insert(create_data, local),
:ok <- maybe_federate(activity),
{:ok, _actor} <- User.increase_note_count(actor) do
{:ok, activity}
end
end
def accept(%{to: to, actor: actor, object: object} = params) do
# only accept false as false value
local = !(params[:local] == false)
with data <- %{"to" => to, "type" => "Accept", "actor" => actor, "object" => object},
{:ok, activity} <- insert(data, local),
:ok <- maybe_federate(activity) do
{:ok, activity}
end
end
def reject(%{to: to, actor: actor, object: object} = params) do
# only accept false as false value
local = !(params[:local] == false)
with data <- %{"to" => to, "type" => "Reject", "actor" => actor, "object" => object},
{:ok, activity} <- insert(data, local),
:ok <- maybe_federate(activity) do
{:ok, activity}
end
end
def update(%{to: to, cc: cc, actor: actor, object: object} = params) do
# only accept false as false value
local = !(params[:local] == false)
with data <- %{
"to" => to,
"cc" => cc,
"type" => "Update",
"actor" => actor,
"object" => object
},
{:ok, activity} <- insert(data, local),
:ok <- maybe_federate(activity) do
{:ok, activity}
end
end
# TODO: This is weird, maybe we shouldn't check here if we can make the activity.
def like(
%User{ap_id: ap_id} = user,
%Object{data: %{"id" => _}} = object,
activity_id \\ nil,
local \\ true
) do
with nil <- get_existing_like(ap_id, object),
like_data <- make_like_data(user, object, activity_id),
{:ok, activity} <- insert(like_data, local),
{:ok, object} <- add_like_to_object(activity, object),
:ok <- maybe_federate(activity) do
{:ok, activity, object}
else
%Activity{} = activity -> {:ok, activity, object}
error -> {:error, error}
end
end
def unlike(
%User{} = actor,
%Object{} = object,
activity_id \\ nil,
local \\ true
) do
with %Activity{} = like_activity <- get_existing_like(actor.ap_id, object),
unlike_data <- make_unlike_data(actor, like_activity, activity_id),
{:ok, unlike_activity} <- insert(unlike_data, local),
{:ok, _activity} <- Repo.delete(like_activity),
{:ok, object} <- remove_like_from_object(like_activity, object),
:ok <- maybe_federate(unlike_activity) do
{:ok, unlike_activity, like_activity, object}
else
_e -> {:ok, object}
end
end
def announce(
%User{ap_id: _} = user,
%Object{data: %{"id" => _}} = object,
activity_id \\ nil,
local \\ true
) do
with true <- is_public?(object),
announce_data <- make_announce_data(user, object, activity_id),
{:ok, activity} <- insert(announce_data, local),
{:ok, object} <- add_announce_to_object(activity, object),
:ok <- maybe_federate(activity) do
{:ok, activity, object}
else
error -> {:error, error}
end
end
def unannounce(
%User{} = actor,
%Object{} = object,
activity_id \\ nil,
local \\ true
) do
with %Activity{} = announce_activity <- get_existing_announce(actor.ap_id, object),
unannounce_data <- make_unannounce_data(actor, announce_activity, activity_id),
{:ok, unannounce_activity} <- insert(unannounce_data, local),
:ok <- maybe_federate(unannounce_activity),
{:ok, _activity} <- Repo.delete(announce_activity),
{:ok, object} <- remove_announce_from_object(announce_activity, object) do
{:ok, unannounce_activity, announce_activity, object}
else
_e -> {:ok, object}
end
end
def follow(follower, followed, activity_id \\ nil, local \\ true) do
with data <- make_follow_data(follower, followed, activity_id),
{:ok, activity} <- insert(data, local),
:ok <- maybe_federate(activity) do
{:ok, activity}
end
end
def unfollow(follower, followed, activity_id \\ nil, local \\ true) do
with %Activity{} = follow_activity <- fetch_latest_follow(follower, followed),
+ {:ok, follow_activity} <- update_follow_state(follow_activity, "cancelled"),
unfollow_data <- make_unfollow_data(follower, followed, follow_activity, activity_id),
{:ok, activity} <- insert(unfollow_data, local),
:ok <- maybe_federate(activity) do
{:ok, activity}
end
end
def delete(%Object{data: %{"id" => id, "actor" => actor}} = object, local \\ true) do
user = User.get_cached_by_ap_id(actor)
data = %{
"type" => "Delete",
"actor" => actor,
"object" => id,
"to" => [user.follower_address, "https://www.w3.org/ns/activitystreams#Public"]
}
with Repo.delete(object),
Repo.delete_all(Activity.all_non_create_by_object_ap_id_q(id)),
{:ok, activity} <- insert(data, local),
:ok <- maybe_federate(activity),
{:ok, _actor} <- User.decrease_note_count(user) do
{:ok, activity}
end
end
def block(blocker, blocked, activity_id \\ nil, local \\ true) do
follow_activity = fetch_latest_follow(blocker, blocked)
if follow_activity do
unfollow(blocker, blocked, nil, local)
end
with block_data <- make_block_data(blocker, blocked, activity_id),
{:ok, activity} <- insert(block_data, local),
:ok <- maybe_federate(activity) do
{:ok, activity}
end
end
def unblock(blocker, blocked, activity_id \\ nil, local \\ true) do
with %Activity{} = block_activity <- fetch_latest_block(blocker, blocked),
unblock_data <- make_unblock_data(blocker, blocked, block_activity, activity_id),
{:ok, activity} <- insert(unblock_data, local),
:ok <- maybe_federate(activity) do
{:ok, activity}
end
end
def fetch_activities_for_context(context, opts \\ %{}) do
public = ["https://www.w3.org/ns/activitystreams#Public"]
recipients =
if opts["user"], do: [opts["user"].ap_id | opts["user"].following] ++ public, else: public
query = from(activity in Activity)
query =
query
|> restrict_blocked(opts)
|> restrict_recipients(recipients, opts["user"])
query =
from(
activity in query,
where:
fragment(
"?->>'type' = ? and ?->>'context' = ?",
activity.data,
"Create",
activity.data,
^context
),
order_by: [desc: :id]
)
Repo.all(query)
end
def fetch_public_activities(opts \\ %{}) do
q = fetch_activities_query(["https://www.w3.org/ns/activitystreams#Public"], opts)
q
|> restrict_unlisted()
|> Repo.all()
|> Enum.reverse()
end
@valid_visibilities ~w[direct unlisted public private]
defp restrict_visibility(query, %{visibility: "direct"}) do
public = "https://www.w3.org/ns/activitystreams#Public"
from(
activity in query,
join: sender in User,
on: sender.ap_id == activity.actor,
# Are non-direct statuses with no to/cc possible?
where:
fragment(
"not (? && ?)",
[^public, sender.follower_address],
activity.recipients
)
)
end
defp restrict_visibility(_query, %{visibility: visibility})
when visibility not in @valid_visibilities do
Logger.error("Could not restrict visibility to #{visibility}")
end
defp restrict_visibility(query, _visibility), do: query
def fetch_user_activities(user, reading_user, params \\ %{}) do
params =
params
|> Map.put("type", ["Create", "Announce"])
|> Map.put("actor_id", user.ap_id)
|> Map.put("whole_db", true)
recipients =
if reading_user do
["https://www.w3.org/ns/activitystreams#Public"] ++
[reading_user.ap_id | reading_user.following]
else
["https://www.w3.org/ns/activitystreams#Public"]
end
fetch_activities(recipients, params)
|> Enum.reverse()
end
defp restrict_since(query, %{"since_id" => since_id}) do
from(activity in query, where: activity.id > ^since_id)
end
defp restrict_since(query, _), do: query
defp restrict_tag(query, %{"tag" => tag}) do
from(
activity in query,
where: fragment("? <@ (? #> '{\"object\",\"tag\"}')", ^tag, activity.data)
)
end
defp restrict_tag(query, _), do: query
defp restrict_recipients(query, [], _user), do: query
defp restrict_recipients(query, recipients, nil) do
from(activity in query, where: fragment("? && ?", ^recipients, activity.recipients))
end
defp restrict_recipients(query, recipients, user) do
from(
activity in query,
where: fragment("? && ?", ^recipients, activity.recipients),
or_where: activity.actor == ^user.ap_id
)
end
defp restrict_limit(query, %{"limit" => limit}) do
from(activity in query, limit: ^limit)
end
defp restrict_limit(query, _), do: query
defp restrict_local(query, %{"local_only" => true}) do
from(activity in query, where: activity.local == true)
end
defp restrict_local(query, _), do: query
defp restrict_max(query, %{"max_id" => max_id}) do
from(activity in query, where: activity.id < ^max_id)
end
defp restrict_max(query, _), do: query
defp restrict_actor(query, %{"actor_id" => actor_id}) do
from(activity in query, where: activity.actor == ^actor_id)
end
defp restrict_actor(query, _), do: query
defp restrict_type(query, %{"type" => type}) when is_binary(type) do
restrict_type(query, %{"type" => [type]})
end
defp restrict_type(query, %{"type" => type}) do
from(activity in query, where: fragment("?->>'type' = ANY(?)", activity.data, ^type))
end
defp restrict_type(query, _), do: query
defp restrict_favorited_by(query, %{"favorited_by" => ap_id}) do
from(
activity in query,
where: fragment("? <@ (? #> '{\"object\",\"likes\"}')", ^ap_id, activity.data)
)
end
defp restrict_favorited_by(query, _), do: query
defp restrict_media(query, %{"only_media" => val}) when val == "true" or val == "1" do
from(
activity in query,
where: fragment("not (? #> '{\"object\",\"attachment\"}' = ?)", activity.data, ^[])
)
end
defp restrict_media(query, _), do: query
# Only search through last 100_000 activities by default
defp restrict_recent(query, %{"whole_db" => true}), do: query
defp restrict_recent(query, _) do
since = (Repo.aggregate(Activity, :max, :id) || 0) - 100_000
from(activity in query, where: activity.id > ^since)
end
defp restrict_blocked(query, %{"blocking_user" => %User{info: info}}) do
blocks = info["blocks"] || []
domain_blocks = info["domain_blocks"] || []
from(
activity in query,
where: fragment("not (? = ANY(?))", activity.actor, ^blocks),
where: fragment("not (?->'to' \\?| ?)", activity.data, ^blocks),
where: fragment("not (split_part(?, '/', 3) = ANY(?))", activity.actor, ^domain_blocks)
)
end
defp restrict_blocked(query, _), do: query
defp restrict_unlisted(query) do
from(
activity in query,
where:
fragment(
"not (coalesce(?->'cc', '{}'::jsonb) \\?| ?)",
activity.data,
^["https://www.w3.org/ns/activitystreams#Public"]
)
)
end
def fetch_activities_query(recipients, opts \\ %{}) do
base_query =
from(
activity in Activity,
limit: 20,
order_by: [fragment("? desc nulls last", activity.id)]
)
base_query
|> restrict_recipients(recipients, opts["user"])
|> restrict_tag(opts)
|> restrict_since(opts)
|> restrict_local(opts)
|> restrict_limit(opts)
|> restrict_max(opts)
|> restrict_actor(opts)
|> restrict_type(opts)
|> restrict_favorited_by(opts)
|> restrict_recent(opts)
|> restrict_blocked(opts)
|> restrict_media(opts)
|> restrict_visibility(opts)
end
def fetch_activities(recipients, opts \\ %{}) do
fetch_activities_query(recipients, opts)
|> Repo.all()
|> Enum.reverse()
end
def upload(file) do
data = Upload.store(file)
Repo.insert(%Object{data: data})
end
def user_data_from_user_object(data) do
avatar =
data["icon"]["url"] &&
%{
"type" => "Image",
"url" => [%{"href" => data["icon"]["url"]}]
}
banner =
data["image"]["url"] &&
%{
"type" => "Image",
"url" => [%{"href" => data["image"]["url"]}]
}
locked = data["manuallyApprovesFollowers"] || false
data = Transmogrifier.maybe_fix_user_object(data)
user_data = %{
ap_id: data["id"],
info: %{
"ap_enabled" => true,
"source_data" => data,
"banner" => banner,
"locked" => locked
},
avatar: avatar,
nickname: "#{data["preferredUsername"]}@#{URI.parse(data["id"]).host}",
name: data["name"],
follower_address: data["followers"],
bio: data["summary"]
}
{:ok, user_data}
end
def fetch_and_prepare_user_from_ap_id(ap_id) do
with {:ok, %{status_code: 200, body: body}} <-
@httpoison.get(ap_id, Accept: "application/activity+json"),
{:ok, data} <- Jason.decode(body) do
user_data_from_user_object(data)
else
e -> Logger.error("Could not decode user at fetch #{ap_id}, #{inspect(e)}")
end
end
def make_user_from_ap_id(ap_id) do
if _user = User.get_by_ap_id(ap_id) do
Transmogrifier.upgrade_user_from_ap_id(ap_id)
else
with {:ok, data} <- fetch_and_prepare_user_from_ap_id(ap_id) do
User.insert_or_update_user(data)
else
e -> {:error, e}
end
end
end
def make_user_from_nickname(nickname) do
with {:ok, %{"ap_id" => ap_id}} when not is_nil(ap_id) <- WebFinger.finger(nickname) do
make_user_from_ap_id(ap_id)
else
_e -> {:error, "No AP id in WebFinger"}
end
end
@quarantined_instances Keyword.get(@instance, :quarantined_instances, [])
def should_federate?(inbox, public) do
if public do
true
else
inbox_info = URI.parse(inbox)
inbox_info.host not in @quarantined_instances
end
end
def publish(actor, activity) do
followers =
if actor.follower_address in activity.recipients do
{:ok, followers} = User.get_followers(actor)
followers |> Enum.filter(&(!&1.local))
else
[]
end
public = is_public?(activity)
remote_inboxes =
(Pleroma.Web.Salmon.remote_users(activity) ++ followers)
|> Enum.filter(fn user -> User.ap_enabled?(user) end)
|> Enum.map(fn %{info: %{"source_data" => data}} ->
(data["endpoints"] && data["endpoints"]["sharedInbox"]) || data["inbox"]
end)
|> Enum.uniq()
|> Enum.filter(fn inbox -> should_federate?(inbox, public) end)
{:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
json = Jason.encode!(data)
Enum.each(remote_inboxes, fn inbox ->
Federator.enqueue(:publish_single_ap, %{
inbox: inbox,
json: json,
actor: actor,
id: activity.data["id"]
})
end)
end
def publish_one(%{inbox: inbox, json: json, actor: actor, id: id}) do
Logger.info("Federating #{id} to #{inbox}")
host = URI.parse(inbox).host
signature =
Pleroma.Web.HTTPSignatures.sign(actor, %{host: host, "content-length": byte_size(json)})
@httpoison.post(
inbox,
json,
[{"Content-Type", "application/activity+json"}, {"signature", signature}],
hackney: [pool: :default]
)
end
# TODO:
# This will create a Create activity, which we need internally at the moment.
def fetch_object_from_id(id) do
if object = Object.get_cached_by_ap_id(id) do
{:ok, object}
else
Logger.info("Fetching #{id} via AP")
with true <- String.starts_with?(id, "http"),
{:ok, %{body: body, status_code: code}} when code in 200..299 <-
@httpoison.get(
id,
[Accept: "application/activity+json"],
follow_redirect: true,
timeout: 10000,
recv_timeout: 20000
),
{:ok, data} <- Jason.decode(body),
nil <- Object.get_by_ap_id(data["id"]),
params <- %{
"type" => "Create",
"to" => data["to"],
"cc" => data["cc"],
"actor" => data["attributedTo"],
"object" => data
},
{:ok, activity} <- Transmogrifier.handle_incoming(params) do
{:ok, Object.get_by_ap_id(activity.data["object"]["id"])}
else
object = %Object{} ->
{:ok, object}
_e ->
Logger.info("Couldn't get object via AP, trying out OStatus fetching...")
case OStatus.fetch_activity_from_url(id) do
{:ok, [activity | _]} -> {:ok, Object.get_by_ap_id(activity.data["object"]["id"])}
e -> e
end
end
end
end
def is_public?(activity) do
"https://www.w3.org/ns/activitystreams#Public" in (activity.data["to"] ++
(activity.data["cc"] || []))
end
def visible_for_user?(activity, nil) do
is_public?(activity)
end
def visible_for_user?(activity, user) do
x = [user.ap_id | user.following]
y = activity.data["to"] ++ (activity.data["cc"] || [])
visible_for_user?(activity, nil) || Enum.any?(x, &(&1 in y))
end
end
diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex
index 75ba36729..e7a3420d2 100644
--- a/lib/pleroma/web/activity_pub/transmogrifier.ex
+++ b/lib/pleroma/web/activity_pub/transmogrifier.ex
@@ -1,650 +1,710 @@
defmodule Pleroma.Web.ActivityPub.Transmogrifier do
@moduledoc """
A module to handle coding from internal to wire ActivityPub and back.
"""
alias Pleroma.User
alias Pleroma.Object
alias Pleroma.Activity
alias Pleroma.Repo
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils
import Ecto.Query
require Logger
@doc """
Modifies an incoming AP object (mastodon format) to our internal format.
"""
def fix_object(object) do
object
|> Map.put("actor", object["attributedTo"])
|> fix_attachments
|> fix_context
|> fix_in_reply_to
|> fix_emoji
|> fix_tag
end
def fix_in_reply_to(%{"inReplyTo" => in_reply_to_id} = object)
when not is_nil(in_reply_to_id) do
case ActivityPub.fetch_object_from_id(in_reply_to_id) do
{:ok, replied_object} ->
activity = Activity.get_create_activity_by_object_ap_id(replied_object.data["id"])
object
|> Map.put("inReplyTo", replied_object.data["id"])
|> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id)
|> Map.put("inReplyToStatusId", activity.id)
|> Map.put("conversation", replied_object.data["context"] || object["conversation"])
|> Map.put("context", replied_object.data["context"] || object["conversation"])
e ->
Logger.error("Couldn't fetch #{object["inReplyTo"]} #{inspect(e)}")
object
end
end
def fix_in_reply_to(object), do: object
def fix_context(object) do
object
|> Map.put("context", object["conversation"])
end
def fix_attachments(object) do
attachments =
(object["attachment"] || [])
|> Enum.map(fn data ->
url = [%{"type" => "Link", "mediaType" => data["mediaType"], "href" => data["url"]}]
Map.put(data, "url", url)
end)
object
|> Map.put("attachment", attachments)
end
def fix_emoji(object) do
tags = object["tag"] || []
emoji = tags |> Enum.filter(fn data -> data["type"] == "Emoji" and data["icon"] end)
emoji =
emoji
|> Enum.reduce(%{}, fn data, mapping ->
name = data["name"]
name =
if String.starts_with?(name, ":") do
name |> String.slice(1..-2)
else
name
end
mapping |> Map.put(name, data["icon"]["url"])
end)
# we merge mastodon and pleroma emoji into a single mapping, to allow for both wire formats
emoji = Map.merge(object["emoji"] || %{}, emoji)
object
|> Map.put("emoji", emoji)
end
def fix_tag(object) do
tags =
(object["tag"] || [])
|> Enum.filter(fn data -> data["type"] == "Hashtag" and data["name"] end)
|> Enum.map(fn data -> String.slice(data["name"], 1..-1) end)
combined = (object["tag"] || []) ++ tags
object
|> Map.put("tag", combined)
end
# TODO: validate those with a Ecto scheme
# - tags
# - emoji
def handle_incoming(%{"type" => "Create", "object" => %{"type" => "Note"} = object} = data) do
with nil <- Activity.get_create_activity_by_object_ap_id(object["id"]),
%User{} = user <- User.get_or_fetch_by_ap_id(data["actor"]) do
object = fix_object(data["object"])
params = %{
to: data["to"],
object: object,
actor: user,
context: object["conversation"],
local: false,
published: data["published"],
additional:
Map.take(data, [
"cc",
"id"
])
}
ActivityPub.create(params)
else
%Activity{} = activity -> {:ok, activity}
_e -> :error
end
end
def handle_incoming(
%{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data
) do
with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
%User{} = follower <- User.get_or_fetch_by_ap_id(follower),
{:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do
- ActivityPub.accept(%{to: [follower.ap_id], actor: followed.ap_id, object: data, local: true})
+ if not User.locked?(followed) do
+ ActivityPub.accept(%{
+ to: [follower.ap_id],
+ actor: followed.ap_id,
+ object: data,
+ local: true
+ })
+
+ User.follow(follower, followed)
+ end
- User.follow(follower, followed)
{:ok, activity}
else
_e -> :error
end
end
defp mastodon_follow_hack(%{"id" => id, "actor" => follower_id}, followed) do
with true <- id =~ "follows",
%User{local: true} = follower <- User.get_cached_by_ap_id(follower_id),
%Activity{} = activity <- Utils.fetch_latest_follow(follower, followed) do
{:ok, activity}
else
_ -> {:error, nil}
end
end
defp mastodon_follow_hack(_), do: {:error, nil}
defp get_follow_activity(follow_object, followed) do
with object_id when not is_nil(object_id) <- Utils.get_ap_id(follow_object),
{_, %Activity{} = activity} <- {:activity, Activity.get_by_ap_id(object_id)} do
{:ok, activity}
else
# Can't find the activity. This might a Mastodon 2.3 "Accept"
{:activity, nil} ->
mastodon_follow_hack(follow_object, followed)
_ ->
{:error, nil}
end
end
def handle_incoming(
%{"type" => "Accept", "object" => follow_object, "actor" => actor, "id" => id} = data
) do
with %User{} = followed <- User.get_or_fetch_by_ap_id(actor),
{:ok, follow_activity} <- get_follow_activity(follow_object, followed),
%User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
{:ok, activity} <-
ActivityPub.accept(%{
to: follow_activity.data["to"],
type: "Accept",
actor: followed.ap_id,
object: follow_activity.data["id"],
local: false
}) do
if not User.following?(follower, followed) do
{:ok, follower} = User.follow(follower, followed)
end
{:ok, activity}
else
_e -> :error
end
end
def handle_incoming(
%{"type" => "Reject", "object" => follow_object, "actor" => actor, "id" => id} = data
) do
with %User{} = followed <- User.get_or_fetch_by_ap_id(actor),
{:ok, follow_activity} <- get_follow_activity(follow_object, followed),
%User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
{:ok, activity} <-
ActivityPub.accept(%{
to: follow_activity.data["to"],
type: "Accept",
actor: followed.ap_id,
object: follow_activity.data["id"],
local: false
}) do
User.unfollow(follower, followed)
{:ok, activity}
else
_e -> :error
end
end
def handle_incoming(
%{"type" => "Like", "object" => object_id, "actor" => actor, "id" => id} = _data
) do
with %User{} = actor <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <-
get_obj_helper(object_id) || ActivityPub.fetch_object_from_id(object_id),
{:ok, activity, _object} <- ActivityPub.like(actor, object, id, false) do
{:ok, activity}
else
_e -> :error
end
end
def handle_incoming(
%{"type" => "Announce", "object" => object_id, "actor" => actor, "id" => id} = _data
) do
with %User{} = actor <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <-
get_obj_helper(object_id) || ActivityPub.fetch_object_from_id(object_id),
{:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false) do
{:ok, activity}
else
_e -> :error
end
end
def handle_incoming(
%{"type" => "Update", "object" => %{"type" => "Person"} = object, "actor" => actor_id} =
data
) do
with %User{ap_id: ^actor_id} = actor <- User.get_by_ap_id(object["id"]) do
{:ok, new_user_data} = ActivityPub.user_data_from_user_object(object)
banner = new_user_data[:info]["banner"]
- locked = new_user_data[:info]["locked"]
+ locked = new_user_data[:info]["locked"] || false
update_data =
new_user_data
|> Map.take([:name, :bio, :avatar])
|> Map.put(:info, Map.merge(actor.info, %{"banner" => banner, "locked" => locked}))
actor
|> User.upgrade_changeset(update_data)
|> User.update_and_set_cache()
ActivityPub.update(%{
local: false,
to: data["to"] || [],
cc: data["cc"] || [],
object: object,
actor: actor_id
})
else
e ->
Logger.error(e)
:error
end
end
# TODO: Make secure.
def handle_incoming(
%{"type" => "Delete", "object" => object_id, "actor" => actor, "id" => _id} = _data
) do
object_id = Utils.get_ap_id(object_id)
with %User{} = _actor <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <-
get_obj_helper(object_id) || ActivityPub.fetch_object_from_id(object_id),
{:ok, activity} <- ActivityPub.delete(object, false) do
{:ok, activity}
else
_e -> :error
end
end
def handle_incoming(
%{
"type" => "Undo",
"object" => %{"type" => "Announce", "object" => object_id},
"actor" => actor,
"id" => id
} = _data
) do
with %User{} = actor <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <-
get_obj_helper(object_id) || ActivityPub.fetch_object_from_id(object_id),
{:ok, activity, _, _} <- ActivityPub.unannounce(actor, object, id, false) do
{:ok, activity}
else
_e -> :error
end
end
def handle_incoming(
%{
"type" => "Undo",
"object" => %{"type" => "Follow", "object" => followed},
"actor" => follower,
"id" => id
} = _data
) do
with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
%User{} = follower <- User.get_or_fetch_by_ap_id(follower),
{:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
User.unfollow(follower, followed)
{:ok, activity}
else
e -> :error
end
end
@ap_config Application.get_env(:pleroma, :activitypub)
@accept_blocks Keyword.get(@ap_config, :accept_blocks)
def handle_incoming(
%{
"type" => "Undo",
"object" => %{"type" => "Block", "object" => blocked},
"actor" => blocker,
"id" => id
} = _data
) do
with true <- @accept_blocks,
%User{local: true} = blocked <- User.get_cached_by_ap_id(blocked),
%User{} = blocker <- User.get_or_fetch_by_ap_id(blocker),
{:ok, activity} <- ActivityPub.unblock(blocker, blocked, id, false) do
User.unblock(blocker, blocked)
{:ok, activity}
else
e -> :error
end
end
def handle_incoming(
%{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = data
) do
with true <- @accept_blocks,
%User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
%User{} = blocker = User.get_or_fetch_by_ap_id(blocker),
{:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do
User.unfollow(blocker, blocked)
User.block(blocker, blocked)
{:ok, activity}
else
e -> :error
end
end
def handle_incoming(
%{
"type" => "Undo",
"object" => %{"type" => "Like", "object" => object_id},
"actor" => actor,
"id" => id
} = _data
) do
with %User{} = actor <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <-
get_obj_helper(object_id) || ActivityPub.fetch_object_from_id(object_id),
{:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do
{:ok, activity}
else
_e -> :error
end
end
def handle_incoming(_), do: :error
def get_obj_helper(id) do
if object = Object.get_by_ap_id(id), do: {:ok, object}, else: nil
end
def set_reply_to_uri(%{"inReplyTo" => inReplyTo} = object) do
with false <- String.starts_with?(inReplyTo, "http"),
{:ok, %{data: replied_to_object}} <- get_obj_helper(inReplyTo) do
Map.put(object, "inReplyTo", replied_to_object["external_url"] || inReplyTo)
else
_e -> object
end
end
def set_reply_to_uri(obj), do: obj
# Prepares the object of an outgoing create activity.
def prepare_object(object) do
object
|> set_sensitive
|> add_hashtags
|> add_mention_tags
|> add_emoji_tags
|> add_attributed_to
|> prepare_attachments
|> set_conversation
|> set_reply_to_uri
end
# @doc
# """
# internal -> Mastodon
# """
def prepare_outgoing(%{"type" => "Create", "object" => %{"type" => "Note"} = object} = data) do
object =
object
|> prepare_object
data =
data
|> Map.put("object", object)
|> Map.put("@context", "https://www.w3.org/ns/activitystreams")
{:ok, data}
end
+ # Mastodon Accept/Reject requires a non-normalized object containing the actor URIs,
+ # because of course it does.
+ def prepare_outgoing(%{"type" => "Accept"} = data) do
+ follow_activity_id =
+ if is_binary(data["object"]) do
+ data["object"]
+ else
+ data["object"]["id"]
+ end
+
+ with follow_activity <- Activity.get_by_ap_id(follow_activity_id) do
+ object = %{
+ "actor" => follow_activity.actor,
+ "object" => follow_activity.data["object"],
+ "id" => follow_activity.data["id"],
+ "type" => "Follow"
+ }
+
+ data =
+ data
+ |> Map.put("object", object)
+ |> Map.put("@context", "https://www.w3.org/ns/activitystreams")
+
+ {:ok, data}
+ end
+ end
+
+ def prepare_outgoing(%{"type" => "Reject"} = data) do
+ follow_activity_id =
+ if is_binary(data["object"]) do
+ data["object"]
+ else
+ data["object"]["id"]
+ end
+
+ with follow_activity <- Activity.get_by_ap_id(follow_activity_id) do
+ object = %{
+ "actor" => follow_activity.actor,
+ "object" => follow_activity.data["object"],
+ "id" => follow_activity.data["id"],
+ "type" => "Follow"
+ }
+
+ data =
+ data
+ |> Map.put("object", object)
+ |> Map.put("@context", "https://www.w3.org/ns/activitystreams")
+
+ {:ok, data}
+ end
+ end
+
def prepare_outgoing(%{"type" => _type} = data) do
data =
data
|> maybe_fix_object_url
|> Map.put("@context", "https://www.w3.org/ns/activitystreams")
{:ok, data}
end
def maybe_fix_object_url(data) do
if is_binary(data["object"]) and not String.starts_with?(data["object"], "http") do
case ActivityPub.fetch_object_from_id(data["object"]) do
{:ok, relative_object} ->
if relative_object.data["external_url"] do
_data =
data
|> Map.put("object", relative_object.data["external_url"])
else
data
end
e ->
Logger.error("Couldn't fetch #{data["object"]} #{inspect(e)}")
data
end
else
data
end
end
def add_hashtags(object) do
tags =
(object["tag"] || [])
|> Enum.map(fn tag ->
%{
"href" => Pleroma.Web.Endpoint.url() <> "/tags/#{tag}",
"name" => "##{tag}",
"type" => "Hashtag"
}
end)
object
|> Map.put("tag", tags)
end
def add_mention_tags(object) do
recipients = object["to"] ++ (object["cc"] || [])
mentions =
recipients
|> Enum.map(fn ap_id -> User.get_cached_by_ap_id(ap_id) end)
|> Enum.filter(& &1)
|> Enum.map(fn user ->
%{"type" => "Mention", "href" => user.ap_id, "name" => "@#{user.nickname}"}
end)
tags = object["tag"] || []
object
|> Map.put("tag", tags ++ mentions)
end
# TODO: we should probably send mtime instead of unix epoch time for updated
def add_emoji_tags(object) do
tags = object["tag"] || []
emoji = object["emoji"] || []
out =
emoji
|> Enum.map(fn {name, url} ->
%{
"icon" => %{"url" => url, "type" => "Image"},
"name" => ":" <> name <> ":",
"type" => "Emoji",
"updated" => "1970-01-01T00:00:00Z",
"id" => url
}
end)
object
|> Map.put("tag", tags ++ out)
end
def set_conversation(object) do
Map.put(object, "conversation", object["context"])
end
def set_sensitive(object) do
tags = object["tag"] || []
Map.put(object, "sensitive", "nsfw" in tags)
end
def add_attributed_to(object) do
attributedTo = object["attributedTo"] || object["actor"]
object
|> Map.put("attributedTo", attributedTo)
end
def prepare_attachments(object) do
attachments =
(object["attachment"] || [])
|> Enum.map(fn data ->
[%{"mediaType" => media_type, "href" => href} | _] = data["url"]
%{"url" => href, "mediaType" => media_type, "name" => data["name"], "type" => "Document"}
end)
object
|> Map.put("attachment", attachments)
end
defp user_upgrade_task(user) do
old_follower_address = User.ap_followers(user)
q =
from(
u in User,
where: ^old_follower_address in u.following,
update: [
set: [
following:
fragment(
"array_replace(?,?,?)",
u.following,
^old_follower_address,
^user.follower_address
)
]
]
)
Repo.update_all(q, [])
maybe_retire_websub(user.ap_id)
# Only do this for recent activties, don't go through the whole db.
# Only look at the last 1000 activities.
since = (Repo.aggregate(Activity, :max, :id) || 0) - 1_000
q =
from(
a in Activity,
where: ^old_follower_address in a.recipients,
where: a.id > ^since,
update: [
set: [
recipients:
fragment(
"array_replace(?,?,?)",
a.recipients,
^old_follower_address,
^user.follower_address
)
]
]
)
Repo.update_all(q, [])
end
def upgrade_user_from_ap_id(ap_id, async \\ true) do
with %User{local: false} = user <- User.get_by_ap_id(ap_id),
{:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id) do
data =
data
|> Map.put(:info, Map.merge(user.info, data[:info]))
already_ap = User.ap_enabled?(user)
{:ok, user} =
User.upgrade_changeset(user, data)
|> Repo.update()
if !already_ap do
# This could potentially take a long time, do it in the background
if async do
Task.start(fn ->
user_upgrade_task(user)
end)
else
user_upgrade_task(user)
end
end
{:ok, user}
else
e -> e
end
end
def maybe_retire_websub(ap_id) do
# some sanity checks
if is_binary(ap_id) && String.length(ap_id) > 8 do
q =
from(
ws in Pleroma.Web.Websub.WebsubClientSubscription,
where: fragment("? like ?", ws.topic, ^"#{ap_id}%")
)
Repo.delete_all(q)
end
end
def maybe_fix_user_url(data) do
if is_map(data["url"]) do
Map.put(data, "url", data["url"]["href"])
else
data
end
end
def maybe_fix_user_object(data) do
data
|> maybe_fix_user_url
end
end
diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex
index 56b80a8db..64329b710 100644
--- a/lib/pleroma/web/activity_pub/utils.ex
+++ b/lib/pleroma/web/activity_pub/utils.ex
@@ -1,424 +1,445 @@
defmodule Pleroma.Web.ActivityPub.Utils do
alias Pleroma.{Repo, Web, Object, Activity, User}
alias Pleroma.Web.Router.Helpers
alias Pleroma.Web.Endpoint
alias Ecto.{Changeset, UUID}
import Ecto.Query
+ require Logger
# Some implementations send the actor URI as the actor field, others send the entire actor object,
# so figure out what the actor's URI is based on what we have.
def get_ap_id(object) do
case object do
%{"id" => id} -> id
id -> id
end
end
def normalize_params(params) do
Map.put(params, "actor", get_ap_id(params["actor"]))
end
def make_json_ld_header do
%{
"@context" => [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
%{
"manuallyApprovesFollowers" => "as:manuallyApprovesFollowers",
"sensitive" => "as:sensitive",
"Hashtag" => "as:Hashtag",
"ostatus" => "http://ostatus.org#",
"atomUri" => "ostatus:atomUri",
"inReplyToAtomUri" => "ostatus:inReplyToAtomUri",
"conversation" => "ostatus:conversation",
"toot" => "http://joinmastodon.org/ns#",
"Emoji" => "toot:Emoji"
}
]
}
end
def make_date do
DateTime.utc_now() |> DateTime.to_iso8601()
end
def generate_activity_id do
generate_id("activities")
end
def generate_context_id do
generate_id("contexts")
end
def generate_object_id do
Helpers.o_status_url(Endpoint, :object, UUID.generate())
end
def generate_id(type) do
"#{Web.base_url()}/#{type}/#{UUID.generate()}"
end
def create_context(context) do
context = context || generate_id("contexts")
changeset = Object.context_mapping(context)
case Repo.insert(changeset) do
{:ok, object} ->
object
# 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)
end
end
@doc """
Enqueues an activity for federation if it's local
"""
def maybe_federate(%Activity{local: true} = activity) do
priority =
case activity.data["type"] do
"Delete" -> 10
"Create" -> 1
_ -> 5
end
Pleroma.Web.Federator.enqueue(:publish, activity, priority)
:ok
end
def maybe_federate(_), do: :ok
@doc """
Adds an id and a published data if they aren't there,
also adds it to an included object
"""
def lazy_put_activity_defaults(map) do
%{data: %{"id" => context}, id: context_id} = create_context(map["context"])
map =
map
|> Map.put_new_lazy("id", &generate_activity_id/0)
|> Map.put_new_lazy("published", &make_date/0)
|> Map.put_new("context", context)
|> Map.put_new("context_id", context_id)
if is_map(map["object"]) do
object = lazy_put_object_defaults(map["object"], map)
%{map | "object" => object}
else
map
end
end
@doc """
Adds an id and published date if they aren't there.
"""
def lazy_put_object_defaults(map, activity \\ %{}) do
map
|> Map.put_new_lazy("id", &generate_object_id/0)
|> Map.put_new_lazy("published", &make_date/0)
|> Map.put_new("context", activity["context"])
|> Map.put_new("context_id", activity["context_id"])
end
@doc """
Inserts a full object if it is contained in an activity.
"""
def insert_full_object(%{"object" => %{"type" => type} = object_data})
when is_map(object_data) and type in ["Note"] do
with {:ok, _} <- Object.create(object_data) do
:ok
end
end
def insert_full_object(_), do: :ok
def update_object_in_activities(%{data: %{"id" => id}} = object) do
# TODO
# Update activities that already had this. Could be done in a seperate process.
# Alternatively, just don't do this and fetch the current object each time. Most
# could probably be taken from cache.
relevant_activities = Activity.all_by_object_ap_id(id)
Enum.map(relevant_activities, fn activity ->
new_activity_data = activity.data |> Map.put("object", object.data)
changeset = Changeset.change(activity, data: new_activity_data)
Repo.update(changeset)
end)
end
#### Like-related helpers
@doc """
Returns an existing like if a user already liked an object
"""
def get_existing_like(actor, %{data: %{"id" => id}}) do
query =
from(
activity in Activity,
where: fragment("(?)->>'actor' = ?", activity.data, ^actor),
# this is to use the index
where:
fragment(
"coalesce((?)->'object'->>'id', (?)->>'object') = ?",
activity.data,
activity.data,
^id
),
where: fragment("(?)->>'type' = 'Like'", activity.data)
)
Repo.one(query)
end
def make_like_data(%User{ap_id: ap_id} = actor, %{data: %{"id" => id}} = object, activity_id) do
data = %{
"type" => "Like",
"actor" => ap_id,
"object" => id,
"to" => [actor.follower_address, object.data["actor"]],
"cc" => ["https://www.w3.org/ns/activitystreams#Public"],
"context" => object.data["context"]
}
if activity_id, do: Map.put(data, "id", activity_id), else: data
end
def update_element_in_object(property, element, object) do
with new_data <-
object.data
|> Map.put("#{property}_count", length(element))
|> Map.put("#{property}s", element),
changeset <- Changeset.change(object, data: new_data),
{:ok, object} <- Repo.update(changeset),
_ <- update_object_in_activities(object) do
{:ok, object}
end
end
def update_likes_in_object(likes, object) do
update_element_in_object("like", likes, object)
end
def add_like_to_object(%Activity{data: %{"actor" => actor}}, object) do
with likes <- [actor | object.data["likes"] || []] |> Enum.uniq() do
update_likes_in_object(likes, object)
end
end
def remove_like_from_object(%Activity{data: %{"actor" => actor}}, object) do
with likes <- (object.data["likes"] || []) |> List.delete(actor) do
update_likes_in_object(likes, object)
end
end
#### Follow-related helpers
+ @doc """
+ Updates a follow activity's state (for locked accounts).
+ """
+ def update_follow_state(%Activity{} = activity, state) do
+ with new_data <-
+ activity.data
+ |> Map.put("state", state),
+ changeset <- Changeset.change(activity, data: new_data),
+ {:ok, activity} <- Repo.update(changeset) do
+ {:ok, activity}
+ end
+ end
+
@doc """
Makes a follow activity data for the given follower and followed
"""
- def make_follow_data(%User{ap_id: follower_id}, %User{ap_id: followed_id}, activity_id) do
+ def make_follow_data(
+ %User{ap_id: follower_id},
+ %User{ap_id: followed_id} = followed,
+ activity_id
+ ) do
data = %{
"type" => "Follow",
"actor" => follower_id,
"to" => [followed_id],
"cc" => ["https://www.w3.org/ns/activitystreams#Public"],
"object" => followed_id
}
- if activity_id, do: Map.put(data, "id", activity_id), else: data
+ data = if activity_id, do: Map.put(data, "id", activity_id), else: data
+ data = if User.locked?(followed), do: Map.put(data, "state", "pending"), else: data
+
+ data
end
def fetch_latest_follow(%User{ap_id: follower_id}, %User{ap_id: followed_id}) do
query =
from(
activity in Activity,
where:
fragment(
"? ->> 'type' = 'Follow'",
activity.data
),
where: activity.actor == ^follower_id,
where:
fragment(
"? @> ?",
activity.data,
^%{object: followed_id}
),
order_by: [desc: :id],
limit: 1
)
Repo.one(query)
end
#### Announce-related helpers
@doc """
Retruns an existing announce activity if the notice has already been announced
"""
def get_existing_announce(actor, %{data: %{"id" => id}}) do
query =
from(
activity in Activity,
where: activity.actor == ^actor,
# this is to use the index
where:
fragment(
"coalesce((?)->'object'->>'id', (?)->>'object') = ?",
activity.data,
activity.data,
^id
),
where: fragment("(?)->>'type' = 'Announce'", activity.data)
)
Repo.one(query)
end
@doc """
Make announce activity data for the given actor and object
"""
def make_announce_data(
%User{ap_id: ap_id} = user,
%Object{data: %{"id" => id}} = object,
activity_id
) do
data = %{
"type" => "Announce",
"actor" => ap_id,
"object" => id,
"to" => [user.follower_address, object.data["actor"]],
"cc" => ["https://www.w3.org/ns/activitystreams#Public"],
"context" => object.data["context"]
}
if activity_id, do: Map.put(data, "id", activity_id), else: data
end
@doc """
Make unannounce activity data for the given actor and object
"""
def make_unannounce_data(
%User{ap_id: ap_id} = user,
%Activity{data: %{"context" => context}} = activity,
activity_id
) do
data = %{
"type" => "Undo",
"actor" => ap_id,
"object" => activity.data,
"to" => [user.follower_address, activity.data["actor"]],
"cc" => ["https://www.w3.org/ns/activitystreams#Public"],
"context" => context
}
if activity_id, do: Map.put(data, "id", activity_id), else: data
end
def make_unlike_data(
%User{ap_id: ap_id} = user,
%Activity{data: %{"context" => context}} = activity,
activity_id
) do
data = %{
"type" => "Undo",
"actor" => ap_id,
"object" => activity.data,
"to" => [user.follower_address, activity.data["actor"]],
"cc" => ["https://www.w3.org/ns/activitystreams#Public"],
"context" => context
}
if activity_id, do: Map.put(data, "id", activity_id), else: data
end
def add_announce_to_object(%Activity{data: %{"actor" => actor}}, object) do
with announcements <- [actor | object.data["announcements"] || []] |> Enum.uniq() do
update_element_in_object("announcement", announcements, object)
end
end
def remove_announce_from_object(%Activity{data: %{"actor" => actor}}, object) do
with announcements <- (object.data["announcements"] || []) |> List.delete(actor) do
update_element_in_object("announcement", announcements, object)
end
end
#### Unfollow-related helpers
def make_unfollow_data(follower, followed, follow_activity, activity_id) do
data = %{
"type" => "Undo",
"actor" => follower.ap_id,
"to" => [followed.ap_id],
"object" => follow_activity.data
}
if activity_id, do: Map.put(data, "id", activity_id), else: data
end
#### Block-related helpers
def fetch_latest_block(%User{ap_id: blocker_id}, %User{ap_id: blocked_id}) do
query =
from(
activity in Activity,
where:
fragment(
"? ->> 'type' = 'Block'",
activity.data
),
where: activity.actor == ^blocker_id,
where:
fragment(
"? @> ?",
activity.data,
^%{object: blocked_id}
),
order_by: [desc: :id],
limit: 1
)
Repo.one(query)
end
def make_block_data(blocker, blocked, activity_id) do
data = %{
"type" => "Block",
"actor" => blocker.ap_id,
"to" => [blocked.ap_id],
"object" => blocked.ap_id
}
if activity_id, do: Map.put(data, "id", activity_id), else: data
end
def make_unblock_data(blocker, blocked, block_activity, activity_id) do
data = %{
"type" => "Undo",
"actor" => blocker.ap_id,
"to" => [blocked.ap_id],
"object" => block_activity.data
}
if activity_id, do: Map.put(data, "id", activity_id), else: data
end
#### Create-related helpers
def make_create_data(params, additional) do
published = params.published || make_date()
%{
"type" => "Create",
"to" => params.to |> Enum.uniq(),
"actor" => params.actor.ap_id,
"object" => params.object,
"published" => published,
"context" => params.context
}
|> Map.merge(additional)
end
end
diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
index 0f7d4bb6d..922b83ed0 100644
--- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
+++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
@@ -1,954 +1,1016 @@
defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
use Pleroma.Web, :controller
alias Pleroma.{Repo, Activity, User, Notification, Stats}
alias Pleroma.Web
alias Pleroma.Web.MastodonAPI.{StatusView, AccountView, MastodonView, ListView}
alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.{CommonAPI, OStatus}
alias Pleroma.Web.OAuth.{Authorization, Token, App}
alias Comeonin.Pbkdf2
import Ecto.Query
require Logger
action_fallback(:errors)
def create_app(conn, params) do
with cs <- App.register_changeset(%App{}, params) |> IO.inspect(),
{:ok, app} <- Repo.insert(cs) |> IO.inspect() do
res = %{
id: app.id,
client_id: app.client_id,
client_secret: app.client_secret
}
json(conn, res)
end
end
def update_credentials(%{assigns: %{user: user}} = conn, params) do
original_user = user
params =
if bio = params["note"] do
Map.put(params, "bio", bio)
else
params
end
params =
if name = params["display_name"] do
Map.put(params, "name", name)
else
params
end
user =
if avatar = params["avatar"] do
with %Plug.Upload{} <- avatar,
{:ok, object} <- ActivityPub.upload(avatar),
change = Ecto.Changeset.change(user, %{avatar: object.data}),
{:ok, user} = User.update_and_set_cache(change) do
user
else
_e -> user
end
else
user
end
user =
if banner = params["header"] do
with %Plug.Upload{} <- banner,
{:ok, object} <- ActivityPub.upload(banner),
new_info <- Map.put(user.info, "banner", object.data),
change <- User.info_changeset(user, %{info: new_info}),
{:ok, user} <- User.update_and_set_cache(change) do
user
else
_e -> user
end
else
user
end
+ user =
+ if locked = params["locked"] do
+ with locked <- locked == "true",
+ new_info <- Map.put(user.info, "locked", locked),
+ change <- User.info_changeset(user, %{info: new_info}),
+ {:ok, user} <- User.update_and_set_cache(change) do
+ user
+ else
+ _e -> user
+ end
+ else
+ user
+ end
+
with changeset <- User.update_changeset(user, params),
{:ok, user} <- User.update_and_set_cache(changeset) do
if original_user != user do
CommonAPI.update(user)
end
json(conn, AccountView.render("account.json", %{user: user}))
else
_e ->
conn
|> put_status(403)
|> json(%{error: "Invalid request"})
end
end
def verify_credentials(%{assigns: %{user: user}} = conn, _) do
account = AccountView.render("account.json", %{user: user})
json(conn, account)
end
def user(conn, %{"id" => id}) do
with %User{} = user <- Repo.get(User, id) do
account = AccountView.render("account.json", %{user: user})
json(conn, account)
else
_e ->
conn
|> put_status(404)
|> json(%{error: "Can't find user"})
end
end
@instance Application.get_env(:pleroma, :instance)
@mastodon_api_level "2.3.3"
def masto_instance(conn, _params) do
response = %{
uri: Web.base_url(),
title: Keyword.get(@instance, :name),
description: "A Pleroma instance, an alternative fediverse server",
version: "#{@mastodon_api_level} (compatible; #{Keyword.get(@instance, :version)})",
email: Keyword.get(@instance, :email),
urls: %{
streaming_api: String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws")
},
stats: Stats.get_stats(),
thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
max_toot_chars: Keyword.get(@instance, :limit)
}
json(conn, response)
end
def peers(conn, _params) do
json(conn, Stats.get_peers())
end
defp mastodonized_emoji do
Pleroma.Formatter.get_custom_emoji()
|> Enum.map(fn {shortcode, relative_url} ->
url = to_string(URI.merge(Web.base_url(), relative_url))
%{
"shortcode" => shortcode,
"static_url" => url,
"visible_in_picker" => true,
"url" => url
}
end)
end
def custom_emojis(conn, _params) do
mastodon_emoji = mastodonized_emoji()
json(conn, mastodon_emoji)
end
defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
last = List.last(activities)
first = List.first(activities)
if last do
min = last.id
max = first.id
{next_url, prev_url} =
if param do
{
mastodon_api_url(
Pleroma.Web.Endpoint,
method,
param,
Map.merge(params, %{max_id: min})
),
mastodon_api_url(
Pleroma.Web.Endpoint,
method,
param,
Map.merge(params, %{since_id: max})
)
}
else
{
mastodon_api_url(
Pleroma.Web.Endpoint,
method,
Map.merge(params, %{max_id: min})
),
mastodon_api_url(
Pleroma.Web.Endpoint,
method,
Map.merge(params, %{since_id: max})
)
}
end
conn
|> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
else
conn
end
end
def home_timeline(%{assigns: %{user: user}} = conn, params) do
params =
params
|> Map.put("type", ["Create", "Announce"])
|> Map.put("blocking_user", user)
|> Map.put("user", user)
activities =
ActivityPub.fetch_activities([user.ap_id | user.following], params)
|> Enum.reverse()
conn
|> add_link_headers(:home_timeline, activities)
|> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
end
def public_timeline(%{assigns: %{user: user}} = conn, params) do
local_only = params["local"] in [true, "True", "true", "1"]
params =
params
|> Map.put("type", ["Create", "Announce"])
|> Map.put("local_only", local_only)
|> Map.put("blocking_user", user)
activities =
ActivityPub.fetch_public_activities(params)
|> Enum.reverse()
conn
|> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
|> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
end
def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
with %User{} = user <- Repo.get(User, params["id"]) do
# Since Pleroma has no "pinned" posts feature, we'll just set an empty list here
activities =
if params["pinned"] == "true" do
[]
else
ActivityPub.fetch_user_activities(user, reading_user, params)
end
conn
|> add_link_headers(:user_statuses, activities, params["id"])
|> render(StatusView, "index.json", %{
activities: activities,
for: reading_user,
as: :activity
})
end
end
def dm_timeline(%{assigns: %{user: user}} = conn, _params) do
query =
ActivityPub.fetch_activities_query([user.ap_id], %{"type" => "Create", visibility: "direct"})
activities = Repo.all(query)
conn
|> add_link_headers(:dm_timeline, activities)
|> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
end
def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Activity{} = activity <- Repo.get(Activity, id),
true <- ActivityPub.visible_for_user?(activity, user) do
render(conn, StatusView, "status.json", %{activity: activity, for: user})
end
end
def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Activity{} = activity <- Repo.get(Activity, id),
activities <-
ActivityPub.fetch_activities_for_context(activity.data["context"], %{
"blocking_user" => user,
"user" => user
}),
activities <-
activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
activities <-
activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
result = %{
ancestors:
StatusView.render(
"index.json",
for: user,
activities: grouped_activities[true] || [],
as: :activity
)
|> Enum.reverse(),
descendants:
StatusView.render(
"index.json",
for: user,
activities: grouped_activities[false] || [],
as: :activity
)
|> Enum.reverse()
}
json(conn, result)
end
end
def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
when length(media_ids) > 0 do
params =
params
|> Map.put("status", ".")
post_status(conn, params)
end
def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
params =
params
|> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
|> Map.put("no_attachment_links", true)
idempotency_key =
case get_req_header(conn, "idempotency-key") do
[key] -> key
_ -> Ecto.UUID.generate()
end
{:ok, activity} =
Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ -> CommonAPI.post(user, params) end)
render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
end
def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
json(conn, %{})
else
_e ->
conn
|> put_status(403)
|> json(%{error: "Can't delete this post"})
end
end
def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user) do
render(conn, StatusView, "status.json", %{activity: announce, for: user, as: :activity})
end
end
def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
%Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
end
end
def fav_status(%{assigns: %{user: user}} = conn, %{"id" => 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
render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
end
end
def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
%Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
end
end
def notifications(%{assigns: %{user: user}} = conn, params) do
notifications = Notification.for_user(user, params)
result =
Enum.map(notifications, fn x ->
render_notification(user, x)
end)
|> Enum.filter(& &1)
conn
|> add_link_headers(:notifications, notifications)
|> json(result)
end
def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
with {:ok, notification} <- Notification.get(user, id) do
json(conn, render_notification(user, notification))
else
{:error, reason} ->
conn
|> put_resp_content_type("application/json")
|> send_resp(403, Jason.encode!(%{"error" => reason}))
end
end
def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
Notification.clear(user)
json(conn, %{})
end
def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
with {:ok, _notif} <- Notification.dismiss(user, id) do
json(conn, %{})
else
{:error, reason} ->
conn
|> put_resp_content_type("application/json")
|> send_resp(403, Jason.encode!(%{"error" => reason}))
end
end
def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
id = List.wrap(id)
q = from(u in User, where: u.id in ^id)
targets = Repo.all(q)
render(conn, AccountView, "relationships.json", %{user: user, targets: targets})
end
def upload(%{assigns: %{user: _}} = conn, %{"file" => file}) do
with {:ok, object} <- ActivityPub.upload(file) do
data =
object.data
|> Map.put("id", object.id)
render(conn, StatusView, "attachment.json", %{attachment: data})
end
end
def favourited_by(conn, %{"id" => id}) do
with %Activity{data: %{"object" => %{"likes" => likes}}} <- Repo.get(Activity, id) do
q = from(u in User, where: u.ap_id in ^likes)
users = Repo.all(q)
render(conn, AccountView, "accounts.json", %{users: users, as: :user})
else
_ -> json(conn, [])
end
end
def reblogged_by(conn, %{"id" => id}) do
with %Activity{data: %{"object" => %{"announcements" => announces}}} <- Repo.get(Activity, id) do
q = from(u in User, where: u.ap_id in ^announces)
users = Repo.all(q)
render(conn, AccountView, "accounts.json", %{users: users, as: :user})
else
_ -> json(conn, [])
end
end
def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
local_only = params["local"] in [true, "True", "true", "1"]
params =
params
|> Map.put("type", "Create")
|> Map.put("local_only", local_only)
|> Map.put("blocking_user", user)
activities =
ActivityPub.fetch_public_activities(params)
|> Enum.reverse()
conn
|> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
|> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
end
# TODO: Pagination
def followers(conn, %{"id" => id}) do
with %User{} = user <- Repo.get(User, id),
{:ok, followers} <- User.get_followers(user) do
render(conn, AccountView, "accounts.json", %{users: followers, as: :user})
end
end
def following(conn, %{"id" => id}) do
with %User{} = user <- Repo.get(User, id),
{:ok, followers} <- User.get_friends(user) do
render(conn, AccountView, "accounts.json", %{users: followers, as: :user})
end
end
+ def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
+ with {:ok, follow_requests} <- User.get_follow_requests(followed) do
+ render(conn, AccountView, "accounts.json", %{users: follow_requests, as: :user})
+ end
+ end
+
+ def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
+ with %User{} = follower <- Repo.get(User, id),
+ {: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
+ render(conn, AccountView, "relationship.json", %{user: followed, target: follower})
+ else
+ {:error, message} ->
+ conn
+ |> put_resp_content_type("application/json")
+ |> send_resp(403, Jason.encode!(%{"error" => message}))
+ end
+ end
+
+ def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
+ with %User{} = follower <- Repo.get(User, id),
+ %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
+ render(conn, AccountView, "relationship.json", %{user: followed, target: follower})
+ else
+ {:error, message} ->
+ conn
+ |> put_resp_content_type("application/json")
+ |> send_resp(403, Jason.encode!(%{"error" => message}))
+ end
+ end
+
def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
with %User{} = followed <- Repo.get(User, id),
{:ok, follower} <- User.maybe_direct_follow(follower, followed),
{:ok, _activity} <- ActivityPub.follow(follower, followed) do
render(conn, AccountView, "relationship.json", %{user: follower, target: followed})
else
{:error, message} ->
conn
|> put_resp_content_type("application/json")
|> send_resp(403, Jason.encode!(%{"error" => message}))
end
end
def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
with %User{} = followed <- Repo.get_by(User, nickname: uri),
{:ok, follower} <- User.maybe_direct_follow(follower, followed),
{:ok, _activity} <- ActivityPub.follow(follower, followed) do
render(conn, AccountView, "account.json", %{user: followed})
else
{:error, message} ->
conn
|> put_resp_content_type("application/json")
|> send_resp(403, Jason.encode!(%{"error" => message}))
end
end
def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
with %User{} = followed <- Repo.get(User, id),
{:ok, _activity} <- ActivityPub.unfollow(follower, followed),
{:ok, follower, _} <- User.unfollow(follower, followed) do
render(conn, AccountView, "relationship.json", %{user: follower, target: followed})
end
end
def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
with %User{} = blocked <- Repo.get(User, id),
{:ok, blocker} <- User.block(blocker, blocked),
{:ok, _activity} <- ActivityPub.block(blocker, blocked) do
render(conn, AccountView, "relationship.json", %{user: blocker, target: blocked})
else
{:error, message} ->
conn
|> put_resp_content_type("application/json")
|> send_resp(403, Jason.encode!(%{"error" => message}))
end
end
def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
with %User{} = blocked <- Repo.get(User, id),
{:ok, blocker} <- User.unblock(blocker, blocked),
{:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
render(conn, AccountView, "relationship.json", %{user: blocker, target: blocked})
else
{:error, message} ->
conn
|> put_resp_content_type("application/json")
|> send_resp(403, Jason.encode!(%{"error" => message}))
end
end
# TODO: Use proper query
def blocks(%{assigns: %{user: user}} = conn, _) do
with blocked_users <- user.info["blocks"] || [],
accounts <- Enum.map(blocked_users, fn ap_id -> User.get_cached_by_ap_id(ap_id) end) do
res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
json(conn, res)
end
end
def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
json(conn, info["domain_blocks"] || [])
end
def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
User.block_domain(blocker, domain)
json(conn, %{})
end
def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
User.unblock_domain(blocker, domain)
json(conn, %{})
end
def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
accounts = User.search(query, params["resolve"] == "true")
fetched =
if Regex.match?(~r/https?:/, query) do
with {:ok, activities} <- OStatus.fetch_activity_from_url(query) do
activities
|> Enum.filter(fn
%{data: %{"type" => "Create"}} -> true
_ -> false
end)
else
_e -> []
end
end || []
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: 20,
order_by: [desc: :id]
)
statuses = Repo.all(q) ++ fetched
tags =
String.split(query)
|> Enum.uniq()
|> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
|> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
res = %{
"accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
"statuses" =>
StatusView.render("index.json", activities: statuses, for: user, as: :activity),
"hashtags" => tags
}
json(conn, res)
end
def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
accounts = User.search(query, params["resolve"] == "true")
res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
json(conn, res)
end
def favourites(%{assigns: %{user: user}} = conn, _) do
params =
%{}
|> Map.put("type", "Create")
|> Map.put("favorited_by", user.ap_id)
|> Map.put("blocking_user", user)
activities =
ActivityPub.fetch_public_activities(params)
|> Enum.reverse()
conn
|> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
end
def get_lists(%{assigns: %{user: user}} = conn, opts) do
lists = Pleroma.List.for_user(user, opts)
res = ListView.render("lists.json", lists: lists)
json(conn, res)
end
def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
res = ListView.render("list.json", list: list)
json(conn, res)
else
_e -> json(conn, "error")
end
end
def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
{:ok, _list} <- Pleroma.List.delete(list) do
json(conn, %{})
else
_e ->
json(conn, "error")
end
end
def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
res = ListView.render("list.json", list: list)
json(conn, res)
end
end
def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
accounts
|> Enum.each(fn account_id ->
with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
%User{} = followed <- Repo.get(User, account_id) do
Pleroma.List.follow(list, followed)
end
end)
json(conn, %{})
end
def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
accounts
|> Enum.each(fn account_id ->
with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
%User{} = followed <- Repo.get(Pleroma.User, account_id) do
Pleroma.List.unfollow(list, followed)
end
end)
json(conn, %{})
end
def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
{:ok, users} = Pleroma.List.get_following(list) do
render(conn, AccountView, "accounts.json", %{users: users, as: :user})
end
end
def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
{:ok, list} <- Pleroma.List.rename(list, title) do
res = ListView.render("list.json", list: list)
json(conn, res)
else
_e ->
json(conn, "error")
end
end
def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
with %Pleroma.List{title: title, following: following} <- Pleroma.List.get(id, user) do
params =
params
|> Map.put("type", "Create")
|> Map.put("blocking_user", user)
# adding title is a hack to not make empty lists function like a public timeline
activities =
ActivityPub.fetch_activities([title | following], params)
|> Enum.reverse()
conn
|> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
else
_e ->
conn
|> put_status(403)
|> json(%{error: "Error."})
end
end
def index(%{assigns: %{user: user}} = conn, _params) do
token =
conn
|> get_session(:oauth_token)
if user && token do
mastodon_emoji = mastodonized_emoji()
accounts = Map.put(%{}, user.id, AccountView.render("account.json", %{user: user}))
initial_state =
%{
meta: %{
streaming_api_base_url:
String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
access_token: token,
locale: "en",
domain: Pleroma.Web.Endpoint.host(),
admin: "1",
me: "#{user.id}",
unfollow_modal: false,
boost_modal: false,
delete_modal: true,
auto_play_gif: false,
reduce_motion: false
},
compose: %{
me: "#{user.id}",
default_privacy: "public",
default_sensitive: false
},
media_attachments: %{
accept_content_types: [
".jpg",
".jpeg",
".png",
".gif",
".webm",
".mp4",
".m4v",
"image\/jpeg",
"image\/png",
"image\/gif",
"video\/webm",
"video\/mp4"
]
},
settings:
Map.get(user.info, "settings") ||
%{
onboarded: true,
home: %{
shows: %{
reblog: true,
reply: true
}
},
notifications: %{
alerts: %{
follow: true,
favourite: true,
reblog: true,
mention: true
},
shows: %{
follow: true,
favourite: true,
reblog: true,
mention: true
},
sounds: %{
follow: true,
favourite: true,
reblog: true,
mention: true
}
}
},
push_subscription: nil,
accounts: accounts,
custom_emojis: mastodon_emoji,
char_limit: Keyword.get(@instance, :limit)
}
|> Jason.encode!()
conn
|> put_layout(false)
|> render(MastodonView, "index.html", %{initial_state: initial_state})
else
conn
|> redirect(to: "/web/login")
end
end
def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
with new_info <- Map.put(user.info, "settings", settings),
change <- User.info_changeset(user, %{info: new_info}),
{:ok, _user} <- User.update_and_set_cache(change) do
conn
|> json(%{})
else
e ->
conn
|> json(%{error: inspect(e)})
end
end
def login(conn, _) do
conn
|> render(MastodonView, "login.html", %{error: false})
end
defp get_or_make_app() do
with %App{} = app <- Repo.get_by(App, client_name: "Mastodon-Local") do
{:ok, app}
else
_e ->
cs =
App.register_changeset(%App{}, %{
client_name: "Mastodon-Local",
redirect_uris: ".",
scopes: "read,write,follow"
})
Repo.insert(cs)
end
end
def login_post(conn, %{"authorization" => %{"name" => name, "password" => password}}) do
with %User{} = user <- User.get_by_nickname_or_email(name),
true <- Pbkdf2.checkpw(password, user.password_hash),
{:ok, app} <- get_or_make_app(),
{:ok, auth} <- Authorization.create_authorization(app, user),
{:ok, token} <- Token.exchange_token(app, auth) do
conn
|> put_session(:oauth_token, token.token)
|> redirect(to: "/web/getting-started")
else
_e ->
conn
|> render(MastodonView, "login.html", %{error: "Wrong username or password"})
end
end
def logout(conn, _) do
conn
|> clear_session
|> redirect(to: "/")
end
def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
Logger.debug("Unimplemented, returning unmodified relationship")
with %User{} = target <- Repo.get(User, id) do
render(conn, AccountView, "relationship.json", %{user: user, target: target})
end
end
def empty_array(conn, _) do
Logger.debug("Unimplemented, returning an empty array")
json(conn, [])
end
def empty_object(conn, _) do
Logger.debug("Unimplemented, returning an empty object")
json(conn, %{})
end
def render_notification(user, %{id: id, activity: activity, inserted_at: created_at} = _params) do
actor = User.get_cached_by_ap_id(activity.data["actor"])
created_at =
NaiveDateTime.to_iso8601(created_at)
|> String.replace(~r/(\.\d+)?$/, ".000Z", global: false)
case activity.data["type"] do
"Create" ->
%{
id: id,
type: "mention",
created_at: created_at,
account: AccountView.render("account.json", %{user: actor}),
status: StatusView.render("status.json", %{activity: activity, for: user})
}
"Like" ->
liked_activity = Activity.get_create_activity_by_object_ap_id(activity.data["object"])
%{
id: id,
type: "favourite",
created_at: created_at,
account: AccountView.render("account.json", %{user: actor}),
status: StatusView.render("status.json", %{activity: liked_activity, for: user})
}
"Announce" ->
announced_activity = Activity.get_create_activity_by_object_ap_id(activity.data["object"])
%{
id: id,
type: "reblog",
created_at: created_at,
account: AccountView.render("account.json", %{user: actor}),
status: StatusView.render("status.json", %{activity: announced_activity, for: user})
}
"Follow" ->
%{
id: id,
type: "follow",
created_at: created_at,
account: AccountView.render("account.json", %{user: actor})
}
_ ->
nil
end
end
def errors(conn, _) do
conn
|> put_status(500)
|> json("Something went wrong")
end
end
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 57b10bff1..ee6a373d3 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -1,356 +1,363 @@
defmodule Pleroma.Web.Router do
use Pleroma.Web, :router
alias Pleroma.{Repo, User, Web.Router}
@instance Application.get_env(:pleroma, :instance)
@federating Keyword.get(@instance, :federating)
@public Keyword.get(@instance, :public)
@registrations_open Keyword.get(@instance, :registrations_open)
def user_fetcher(username) do
{:ok, Repo.get_by(User, %{nickname: username})}
end
pipeline :api do
plug(:accepts, ["json"])
plug(:fetch_session)
plug(Pleroma.Plugs.OAuthPlug)
plug(Pleroma.Plugs.AuthenticationPlug, %{fetcher: &Router.user_fetcher/1, optional: true})
end
pipeline :authenticated_api do
plug(:accepts, ["json"])
plug(:fetch_session)
plug(Pleroma.Plugs.OAuthPlug)
plug(Pleroma.Plugs.AuthenticationPlug, %{fetcher: &Router.user_fetcher/1})
end
pipeline :mastodon_html do
plug(:accepts, ["html"])
plug(:fetch_session)
plug(Pleroma.Plugs.OAuthPlug)
plug(Pleroma.Plugs.AuthenticationPlug, %{fetcher: &Router.user_fetcher/1, optional: true})
end
pipeline :pleroma_html do
plug(:accepts, ["html"])
plug(:fetch_session)
plug(Pleroma.Plugs.OAuthPlug)
plug(Pleroma.Plugs.AuthenticationPlug, %{fetcher: &Router.user_fetcher/1, optional: true})
end
pipeline :well_known do
plug(:accepts, ["xml", "xrd+xml", "json", "jrd+json"])
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
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)
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)
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("/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("/follow_requests", MastodonAPIController, :empty_array)
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)
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)
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("/search", MastodonAPIController, :search)
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)
if @registrations_open do
post("/account/register", TwitterAPI.Controller, :register)
end
get("/search", TwitterAPI.Controller, :search)
get("/statusnet/tags/timeline/:tag", TwitterAPI.Controller, :public_and_external_timeline)
end
scope "/api", Pleroma.Web do
if @public do
pipe_through(:api)
else
pipe_through(:authenticated_api)
end
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 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)
post(
"/account/most_recent_notification",
TwitterAPI.Controller,
:update_most_recent_notification
)
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("/qvitter/statuses/notifications", TwitterAPI.Controller, :notifications)
post("/statuses/update", TwitterAPI.Controller, :status_update)
post("/statuses/retweet/:id", TwitterAPI.Controller, :retweet)
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("/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("/externalprofile/show", TwitterAPI.Controller, :external_profile)
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)
if @federating do
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
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
if @federating do
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
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
plug(:accepts, ["html"])
end
scope "/proxy/", Pleroma.Web.MediaProxy do
pipe_through(:remote_media)
get("/:sig/:url", MediaProxyController, :remote)
end
scope "/", Fallback do
get("/*path", RedirectController, :redirector)
end
end
defmodule Fallback.RedirectController do
use Pleroma.Web, :controller
def redirector(conn, _params) do
if Mix.env() != :test do
conn
|> put_resp_content_type("text/html")
|> send_file(200, "priv/static/index.html")
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 d53dd0c44..b29687df5 100644
--- a/lib/pleroma/web/twitter_api/twitter_api_controller.ex
+++ b/lib/pleroma/web/twitter_api/twitter_api_controller.ex
@@ -1,409 +1,472 @@
defmodule Pleroma.Web.TwitterAPI.Controller do
use Pleroma.Web, :controller
alias Pleroma.Web.TwitterAPI.{TwitterAPI, UserView, ActivityView, NotificationView}
alias Pleroma.Web.CommonAPI
alias Pleroma.{Repo, Activity, User, Notification}
alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.ActivityPub.Utils
alias Ecto.Changeset
require Logger
action_fallback(:errors)
def verify_credentials(%{assigns: %{user: user}} = conn, _params) do
token = Phoenix.Token.sign(conn, "user socket", user.id)
render(conn, UserView, "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
|> render(ActivityView, "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
|> render(ActivityView, "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)
conn
|> render(ActivityView, "index.json", %{activities: activities, for: user})
end
def show_user(conn, params) do
with {:ok, shown} <- TwitterAPI.get_user(params) do
if user = conn.assigns.user do
render(conn, UserView, "show.json", %{user: shown, for: user})
else
render(conn, UserView, "show.json", %{user: shown})
end
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
|> render(ActivityView, "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
|> render(ActivityView, "index.json", %{activities: activities, for: user})
end
def notifications(%{assigns: %{user: user}} = conn, params) do
notifications = Notification.for_user(user, params)
conn
|> render(NotificationView, "notification.json", %{notifications: notifications, for: user})
end
def follow(%{assigns: %{user: user}} = conn, params) do
case TwitterAPI.follow(user, params) do
{:ok, user, followed, _activity} ->
render(conn, UserView, "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} ->
render(conn, UserView, "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} ->
render(conn, UserView, "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
render(conn, ActivityView, "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} ->
render(conn, UserView, "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
render(conn, ActivityView, "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
|> render(ActivityView, "index.json", %{activities: activities, for: user})
end
end
def upload(conn, %{"media" => media}) do
response = TwitterAPI.upload(media)
conn
|> put_resp_content_type("application/atom+xml")
|> send_resp(200, response)
end
def upload_json(conn, %{"media" => media}) do
response = TwitterAPI.upload(media, "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
render(conn, ActivityView, "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
render(conn, ActivityView, "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
render(conn, ActivityView, "activity.json", %{activity: activity, for: user})
end
end
def register(conn, params) do
with {:ok, user} <- TwitterAPI.register_user(params) do
render(conn, UserView, "show.json", %{user: user})
else
{:error, errors} ->
conn
|> json_reply(400, Jason.encode!(errors))
end
end
def update_avatar(%{assigns: %{user: user}} = conn, params) do
{:ok, object} = ActivityPub.upload(params)
change = Changeset.change(user, %{avatar: object.data})
{:ok, user} = User.update_and_set_cache(change)
CommonAPI.update(user)
render(conn, UserView, "show.json", %{user: user, for: user})
end
def update_banner(%{assigns: %{user: user}} = conn, params) do
with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}),
new_info <- Map.put(user.info, "banner", object.data),
change <- User.info_changeset(user, %{info: new_info}),
{:ok, user} <- User.update_and_set_cache(change) 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),
new_info <- Map.put(user.info, "background", object.data),
change <- User.info_changeset(user, %{info: new_info}),
{:ok, _user} <- User.update_and_set_cache(change) 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 update_most_recent_notification(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with id when is_number(id) <- String.to_integer(id),
info <- user.info,
mrn <- max(id, user.info["most_recent_notification"] || 0),
updated_info <- Map.put(info, "most_recent_notification", mrn),
changeset <- User.info_changeset(user, %{info: updated_info}),
{:ok, _user} <- User.update_and_set_cache(changeset) do
conn
|> json_reply(200, Jason.encode!(mrn))
else
_e -> bad_request_reply(conn, "Can't update.")
end
end
def followers(conn, params) do
with {:ok, user} <- TwitterAPI.get_user(conn.assigns[:user], params),
{:ok, followers} <- User.get_followers(user) do
render(conn, UserView, "index.json", %{users: followers, for: conn.assigns[:user]})
else
_e -> bad_request_reply(conn, "Can't get followers")
end
end
def friends(conn, params) do
with {:ok, user} <- TwitterAPI.get_user(conn.assigns[:user], params),
{:ok, friends} <- User.get_friends(user) do
render(conn, UserView, "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
+ render(conn, UserView, "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
+ render(conn, UserView, "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
+ render(conn, UserView, "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 update_profile(%{assigns: %{user: user}} = conn, params) do
params =
if bio = params["description"] do
bio_brs = Regex.replace(~r/\r?\n/, bio, "<br>")
Map.put(params, "bio", bio_brs)
else
params
end
+ user =
+ if locked = params["locked"] do
+ with locked <- locked == "true",
+ new_info <- Map.put(user.info, "locked", locked),
+ change <- User.info_changeset(user, %{info: new_info}),
+ {:ok, user} <- User.update_and_set_cache(change) do
+ user
+ else
+ _e -> user
+ end
+ else
+ user
+ end
+
with changeset <- User.update_changeset(user, params),
{:ok, user} <- User.update_and_set_cache(changeset) do
CommonAPI.update(user)
render(conn, UserView, "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
|> render(ActivityView, "index.json", %{activities: activities, 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
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
diff --git a/lib/pleroma/web/twitter_api/views/user_view.ex b/lib/pleroma/web/twitter_api/views/user_view.ex
index 31527caae..711008973 100644
--- a/lib/pleroma/web/twitter_api/views/user_view.ex
+++ b/lib/pleroma/web/twitter_api/views/user_view.ex
@@ -1,83 +1,84 @@
defmodule Pleroma.Web.TwitterAPI.UserView do
use Pleroma.Web, :view
alias Pleroma.User
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MediaProxy
def render("show.json", %{user: user = %User{}} = assigns) do
render_one(user, Pleroma.Web.TwitterAPI.UserView, "user.json", assigns)
end
def render("index.json", %{users: users, for: user}) do
render_many(users, Pleroma.Web.TwitterAPI.UserView, "user.json", for: user)
end
def render("user.json", %{user: user = %User{}} = assigns) do
image = User.avatar_url(user) |> MediaProxy.url()
{following, follows_you, statusnet_blocking} =
if assigns[:for] do
{
User.following?(assigns[:for], user),
User.following?(user, assigns[:for]),
User.blocks?(assigns[:for], user)
}
else
{false, false, false}
end
user_info = User.get_cached_user_info(user)
data = %{
"created_at" => user.inserted_at |> Utils.format_naive_asctime(),
"description" => HtmlSanitizeEx.strip_tags(user.bio),
"favourites_count" => 0,
"followers_count" => user_info[:follower_count],
"following" => following,
"follows_you" => follows_you,
"statusnet_blocking" => statusnet_blocking,
"friends_count" => user_info[:following_count],
"id" => user.id,
"name" => user.name,
"profile_image_url" => image,
"profile_image_url_https" => image,
"profile_image_url_profile_size" => image,
"profile_image_url_original" => image,
"rights" => %{
"delete_others_notice" => !!user.info["is_moderator"]
},
"screen_name" => user.nickname,
"statuses_count" => user_info[:note_count],
"statusnet_profile_url" => user.ap_id,
"cover_photo" => User.banner_url(user) |> MediaProxy.url(),
"background_image" => image_url(user.info["background"]) |> MediaProxy.url(),
- "is_local" => user.local
+ "is_local" => user.local,
+ "locked" => !!user.info["locked"]
}
if assigns[:token] do
Map.put(data, "token", assigns[:token])
else
data
end
end
def render("short.json", %{
user: %User{
nickname: nickname,
id: id,
ap_id: ap_id,
name: name
}
}) do
%{
"fullname" => name,
"id" => id,
"ostatus_uri" => ap_id,
"profile_url" => ap_id,
"screen_name" => nickname
}
end
defp image_url(%{"url" => [%{"href" => href} | _]}), do: href
defp image_url(_), do: nil
end
diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs
index 566f5acfc..d1812457d 100644
--- a/test/web/mastodon_api/mastodon_api_controller_test.exs
+++ b/test/web/mastodon_api/mastodon_api_controller_test.exs
@@ -1,1043 +1,1111 @@
defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
use Pleroma.Web.ConnCase
alias Pleroma.Web.TwitterAPI.TwitterAPI
alias Pleroma.{Repo, User, Activity, Notification}
alias Pleroma.Web.{OStatus, CommonAPI}
+ alias Pleroma.Web.ActivityPub.ActivityPub
import Pleroma.Factory
import ExUnit.CaptureLog
test "the home timeline", %{conn: conn} do
user = insert(:user)
following = insert(:user)
{:ok, _activity} = TwitterAPI.create_status(following, %{"status" => "test"})
conn =
conn
|> assign(:user, user)
|> get("/api/v1/timelines/home")
assert length(json_response(conn, 200)) == 0
{:ok, user} = User.follow(user, following)
conn =
build_conn()
|> assign(:user, user)
|> get("/api/v1/timelines/home")
assert [%{"content" => "test"}] = json_response(conn, 200)
end
test "the public timeline", %{conn: conn} do
following = insert(:user)
capture_log(fn ->
{:ok, _activity} = TwitterAPI.create_status(following, %{"status" => "test"})
{:ok, [_activity]} =
OStatus.fetch_activity_from_url("https://shitposter.club/notice/2827873")
conn =
conn
|> get("/api/v1/timelines/public", %{"local" => "False"})
assert length(json_response(conn, 200)) == 2
conn =
build_conn()
|> get("/api/v1/timelines/public", %{"local" => "True"})
assert [%{"content" => "test"}] = json_response(conn, 200)
conn =
build_conn()
|> get("/api/v1/timelines/public", %{"local" => "1"})
assert [%{"content" => "test"}] = json_response(conn, 200)
end)
end
test "posting a status", %{conn: conn} do
user = insert(:user)
idempotency_key = "Pikachu rocks!"
conn_one =
conn
|> assign(:user, user)
|> put_req_header("idempotency-key", idempotency_key)
|> post("/api/v1/statuses", %{
"status" => "cofe",
"spoiler_text" => "2hu",
"sensitive" => "false"
})
{:ok, ttl} = Cachex.ttl(:idempotency_cache, idempotency_key)
# Six hours
assert ttl > :timer.seconds(6 * 60 * 60 - 1)
assert %{"content" => "cofe", "id" => id, "spoiler_text" => "2hu", "sensitive" => false} =
json_response(conn_one, 200)
assert Repo.get(Activity, id)
conn_two =
conn
|> assign(:user, user)
|> put_req_header("idempotency-key", idempotency_key)
|> post("/api/v1/statuses", %{
"status" => "cofe",
"spoiler_text" => "2hu",
"sensitive" => "false"
})
assert %{"id" => second_id} = json_response(conn_two, 200)
assert id == second_id
conn_three =
conn
|> assign(:user, user)
|> post("/api/v1/statuses", %{
"status" => "cofe",
"spoiler_text" => "2hu",
"sensitive" => "false"
})
assert %{"id" => third_id} = json_response(conn_three, 200)
refute id == third_id
end
test "posting a sensitive status", %{conn: conn} do
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> post("/api/v1/statuses", %{"status" => "cofe", "sensitive" => true})
assert %{"content" => "cofe", "id" => id, "sensitive" => true} = json_response(conn, 200)
assert Repo.get(Activity, id)
end
test "posting a direct status", %{conn: conn} do
user1 = insert(:user)
user2 = insert(:user)
content = "direct cofe @#{user2.nickname}"
conn =
conn
|> assign(:user, user1)
|> post("api/v1/statuses", %{"status" => content, "visibility" => "direct"})
assert %{"id" => id, "visibility" => "direct"} = json_response(conn, 200)
assert activity = Repo.get(Activity, id)
assert activity.recipients == [user2.ap_id]
assert activity.data["to"] == [user2.ap_id]
assert activity.data["cc"] == []
end
test "direct timeline", %{conn: conn} do
user_one = insert(:user)
user_two = insert(:user)
{:ok, user_two} = User.follow(user_two, user_one)
{:ok, direct} =
CommonAPI.post(user_one, %{
"status" => "Hi @#{user_two.nickname}!",
"visibility" => "direct"
})
{:ok, _follower_only} =
CommonAPI.post(user_one, %{
"status" => "Hi @#{user_two.nickname}!",
"visibility" => "private"
})
# Only direct should be visible here
res_conn =
conn
|> assign(:user, user_two)
|> get("api/v1/timelines/direct")
[status] = json_response(res_conn, 200)
assert %{"visibility" => "direct"} = status
assert status["url"] != direct.data["id"]
# Both should be visible here
res_conn =
conn
|> assign(:user, user_two)
|> get("api/v1/timelines/home")
[_s1, _s2] = json_response(res_conn, 200)
end
test "replying to a status", %{conn: conn} do
user = insert(:user)
{:ok, replied_to} = TwitterAPI.create_status(user, %{"status" => "cofe"})
conn =
conn
|> assign(:user, user)
|> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => replied_to.id})
assert %{"content" => "xD", "id" => id} = json_response(conn, 200)
activity = Repo.get(Activity, id)
assert activity.data["context"] == replied_to.data["context"]
assert activity.data["object"]["inReplyToStatusId"] == replied_to.id
end
test "verify_credentials", %{conn: conn} do
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> get("/api/v1/accounts/verify_credentials")
assert %{"id" => id} = json_response(conn, 200)
assert id == to_string(user.id)
end
test "get a status", %{conn: conn} do
activity = insert(:note_activity)
conn =
conn
|> get("/api/v1/statuses/#{activity.id}")
assert %{"id" => id} = json_response(conn, 200)
assert id == to_string(activity.id)
end
describe "deleting a status" do
test "when you created it", %{conn: conn} do
activity = insert(:note_activity)
author = User.get_by_ap_id(activity.data["actor"])
conn =
conn
|> assign(:user, author)
|> delete("/api/v1/statuses/#{activity.id}")
assert %{} = json_response(conn, 200)
assert Repo.get(Activity, activity.id) == nil
end
test "when you didn't create it", %{conn: conn} do
activity = insert(:note_activity)
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> delete("/api/v1/statuses/#{activity.id}")
assert %{"error" => _} = json_response(conn, 403)
assert Repo.get(Activity, activity.id) == activity
end
end
describe "lists" do
test "creating a list", %{conn: conn} do
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> post("/api/v1/lists", %{"title" => "cuties"})
assert %{"title" => title} = json_response(conn, 200)
assert title == "cuties"
end
test "adding users to a list", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, list} = Pleroma.List.create("name", user)
conn =
conn
|> assign(:user, user)
|> post("/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]})
assert %{} == json_response(conn, 200)
%Pleroma.List{following: following} = Pleroma.List.get(list.id, user)
assert following == [other_user.follower_address]
end
test "removing users from a list", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
third_user = insert(:user)
{:ok, list} = Pleroma.List.create("name", user)
{:ok, list} = Pleroma.List.follow(list, other_user)
{:ok, list} = Pleroma.List.follow(list, third_user)
conn =
conn
|> assign(:user, user)
|> delete("/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]})
assert %{} == json_response(conn, 200)
%Pleroma.List{following: following} = Pleroma.List.get(list.id, user)
assert following == [third_user.follower_address]
end
test "listing users in a list", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, list} = Pleroma.List.create("name", user)
{:ok, list} = Pleroma.List.follow(list, other_user)
conn =
conn
|> assign(:user, user)
|> get("/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]})
assert [%{"id" => id}] = json_response(conn, 200)
assert id == to_string(other_user.id)
end
test "retrieving a list", %{conn: conn} do
user = insert(:user)
{:ok, list} = Pleroma.List.create("name", user)
conn =
conn
|> assign(:user, user)
|> get("/api/v1/lists/#{list.id}")
assert %{"id" => id} = json_response(conn, 200)
assert id == to_string(list.id)
end
test "renaming a list", %{conn: conn} do
user = insert(:user)
{:ok, list} = Pleroma.List.create("name", user)
conn =
conn
|> assign(:user, user)
|> put("/api/v1/lists/#{list.id}", %{"title" => "newname"})
assert %{"title" => name} = json_response(conn, 200)
assert name == "newname"
end
test "deleting a list", %{conn: conn} do
user = insert(:user)
{:ok, list} = Pleroma.List.create("name", user)
conn =
conn
|> assign(:user, user)
|> delete("/api/v1/lists/#{list.id}")
assert %{} = json_response(conn, 200)
assert is_nil(Repo.get(Pleroma.List, list.id))
end
test "list timeline", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, _activity_one} = TwitterAPI.create_status(user, %{"status" => "Marisa is cute."})
{:ok, activity_two} = TwitterAPI.create_status(other_user, %{"status" => "Marisa is cute."})
{:ok, list} = Pleroma.List.create("name", user)
{:ok, list} = Pleroma.List.follow(list, other_user)
conn =
conn
|> assign(:user, user)
|> get("/api/v1/timelines/list/#{list.id}")
assert [%{"id" => id}] = json_response(conn, 200)
assert id == to_string(activity_two.id)
end
end
describe "notifications" do
test "list of notifications", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} =
TwitterAPI.create_status(other_user, %{"status" => "hi @#{user.nickname}"})
{:ok, [_notification]} = Notification.create_notifications(activity)
conn =
conn
|> assign(:user, user)
|> get("/api/v1/notifications")
expected_response =
"hi <span><a href=\"#{user.ap_id}\">@<span>#{user.nickname}</span></a></span>"
assert [%{"status" => %{"content" => response}} | _rest] = json_response(conn, 200)
assert response == expected_response
end
test "getting a single notification", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} =
TwitterAPI.create_status(other_user, %{"status" => "hi @#{user.nickname}"})
{:ok, [notification]} = Notification.create_notifications(activity)
conn =
conn
|> assign(:user, user)
|> get("/api/v1/notifications/#{notification.id}")
expected_response =
"hi <span><a href=\"#{user.ap_id}\">@<span>#{user.nickname}</span></a></span>"
assert %{"status" => %{"content" => response}} = json_response(conn, 200)
assert response == expected_response
end
test "dismissing a single notification", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} =
TwitterAPI.create_status(other_user, %{"status" => "hi @#{user.nickname}"})
{:ok, [notification]} = Notification.create_notifications(activity)
conn =
conn
|> assign(:user, user)
|> post("/api/v1/notifications/dismiss", %{"id" => notification.id})
assert %{} = json_response(conn, 200)
end
test "clearing all notifications", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} =
TwitterAPI.create_status(other_user, %{"status" => "hi @#{user.nickname}"})
{:ok, [_notification]} = Notification.create_notifications(activity)
conn =
conn
|> assign(:user, user)
|> post("/api/v1/notifications/clear")
assert %{} = json_response(conn, 200)
conn =
build_conn()
|> assign(:user, user)
|> get("/api/v1/notifications")
assert all = json_response(conn, 200)
assert all == []
end
end
describe "reblogging" do
test "reblogs and returns the reblogged status", %{conn: conn} do
activity = insert(:note_activity)
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> post("/api/v1/statuses/#{activity.id}/reblog")
assert %{"reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1}} =
json_response(conn, 200)
assert to_string(activity.id) == id
end
end
describe "unreblogging" do
test "unreblogs and returns the unreblogged status", %{conn: conn} do
activity = insert(:note_activity)
user = insert(:user)
{:ok, _, _} = CommonAPI.repeat(activity.id, user)
conn =
conn
|> assign(:user, user)
|> post("/api/v1/statuses/#{activity.id}/unreblog")
assert %{"id" => id, "reblogged" => false, "reblogs_count" => 0} = json_response(conn, 200)
assert to_string(activity.id) == id
end
end
describe "favoriting" do
test "favs a status and returns it", %{conn: conn} do
activity = insert(:note_activity)
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> post("/api/v1/statuses/#{activity.id}/favourite")
assert %{"id" => id, "favourites_count" => 1, "favourited" => true} =
json_response(conn, 200)
assert to_string(activity.id) == id
end
test "returns 500 for a wrong id", %{conn: conn} do
user = insert(:user)
resp =
conn
|> assign(:user, user)
|> post("/api/v1/statuses/1/favourite")
|> json_response(500)
assert resp == "Something went wrong"
end
end
describe "unfavoriting" do
test "unfavorites a status and returns it", %{conn: conn} do
activity = insert(:note_activity)
user = insert(:user)
{:ok, _, _} = CommonAPI.favorite(activity.id, user)
conn =
conn
|> assign(:user, user)
|> post("/api/v1/statuses/#{activity.id}/unfavourite")
assert %{"id" => id, "favourites_count" => 0, "favourited" => false} =
json_response(conn, 200)
assert to_string(activity.id) == id
end
end
describe "user timelines" do
test "gets a users statuses", %{conn: conn} do
user_one = insert(:user)
user_two = insert(:user)
user_three = insert(:user)
{:ok, user_three} = User.follow(user_three, user_one)
{:ok, activity} = CommonAPI.post(user_one, %{"status" => "HI!!!"})
{:ok, direct_activity} =
CommonAPI.post(user_one, %{
"status" => "Hi, @#{user_two.nickname}.",
"visibility" => "direct"
})
{:ok, private_activity} =
CommonAPI.post(user_one, %{"status" => "private", "visibility" => "private"})
resp =
conn
|> get("/api/v1/accounts/#{user_one.id}/statuses")
assert [%{"id" => id}] = json_response(resp, 200)
assert id == to_string(activity.id)
resp =
conn
|> assign(:user, user_two)
|> get("/api/v1/accounts/#{user_one.id}/statuses")
assert [%{"id" => id_one}, %{"id" => id_two}] = json_response(resp, 200)
assert id_one == to_string(direct_activity.id)
assert id_two == to_string(activity.id)
resp =
conn
|> assign(:user, user_three)
|> get("/api/v1/accounts/#{user_one.id}/statuses")
assert [%{"id" => id_one}, %{"id" => id_two}] = json_response(resp, 200)
assert id_one == to_string(private_activity.id)
assert id_two == to_string(activity.id)
end
test "unimplemented pinned statuses feature", %{conn: conn} do
note = insert(:note_activity)
user = User.get_by_ap_id(note.data["actor"])
conn =
conn
|> get("/api/v1/accounts/#{user.id}/statuses?pinned=true")
assert json_response(conn, 200) == []
end
test "gets an users media", %{conn: conn} do
note = insert(:note_activity)
user = User.get_by_ap_id(note.data["actor"])
file = %Plug.Upload{
content_type: "image/jpg",
path: Path.absname("test/fixtures/image.jpg"),
filename: "an_image.jpg"
}
media =
TwitterAPI.upload(file, "json")
|> Poison.decode!()
{:ok, image_post} =
TwitterAPI.create_status(user, %{"status" => "cofe", "media_ids" => [media["media_id"]]})
conn =
conn
|> get("/api/v1/accounts/#{user.id}/statuses", %{"only_media" => "true"})
assert [%{"id" => id}] = json_response(conn, 200)
assert id == to_string(image_post.id)
conn =
build_conn()
|> get("/api/v1/accounts/#{user.id}/statuses", %{"only_media" => "1"})
assert [%{"id" => id}] = json_response(conn, 200)
assert id == to_string(image_post.id)
end
end
describe "user relationships" do
test "returns the relationships for the current user", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, user} = User.follow(user, other_user)
conn =
conn
|> assign(:user, user)
|> get("/api/v1/accounts/relationships", %{"id" => [other_user.id]})
assert [relationship] = json_response(conn, 200)
assert to_string(other_user.id) == relationship["id"]
end
end
+ describe "locked accounts" do
+ test "/api/v1/follow_requests works" do
+ user = insert(:user, %{info: %{"locked" => true}})
+ other_user = insert(:user)
+
+ {:ok, activity} = ActivityPub.follow(other_user, user)
+
+ user = Repo.get(User, user.id)
+ other_user = Repo.get(User, other_user.id)
+
+ assert User.following?(other_user, user) == false
+
+ conn =
+ build_conn()
+ |> assign(:user, user)
+ |> get("/api/v1/follow_requests")
+
+ assert [relationship] = json_response(conn, 200)
+ assert to_string(other_user.id) == relationship["id"]
+ end
+
+ test "/api/v1/follow_requests/:id/authorize works" do
+ user = insert(:user, %{info: %{"locked" => true}})
+ other_user = insert(:user)
+
+ {:ok, activity} = ActivityPub.follow(other_user, user)
+
+ user = Repo.get(User, user.id)
+ other_user = Repo.get(User, other_user.id)
+
+ assert User.following?(other_user, user) == false
+
+ conn =
+ build_conn()
+ |> assign(:user, user)
+ |> post("/api/v1/follow_requests/#{other_user.id}/authorize")
+
+ assert relationship = json_response(conn, 200)
+ assert to_string(other_user.id) == relationship["id"]
+
+ user = Repo.get(User, user.id)
+ other_user = Repo.get(User, other_user.id)
+
+ assert User.following?(other_user, user) == true
+ end
+
+ test "/api/v1/follow_requests/:id/reject works" do
+ user = insert(:user, %{info: %{"locked" => true}})
+ other_user = insert(:user)
+
+ {:ok, activity} = ActivityPub.follow(other_user, user)
+
+ conn =
+ build_conn()
+ |> assign(:user, user)
+ |> post("/api/v1/follow_requests/#{other_user.id}/reject")
+
+ assert relationship = json_response(conn, 200)
+ assert to_string(other_user.id) == relationship["id"]
+
+ user = Repo.get(User, user.id)
+ other_user = Repo.get(User, other_user.id)
+
+ assert User.following?(other_user, user) == false
+ end
+ end
+
test "account fetching", %{conn: conn} do
user = insert(:user)
conn =
conn
|> get("/api/v1/accounts/#{user.id}")
assert %{"id" => id} = json_response(conn, 200)
assert id == to_string(user.id)
conn =
build_conn()
|> get("/api/v1/accounts/-1")
assert %{"error" => "Can't find user"} = json_response(conn, 404)
end
test "media upload", %{conn: conn} do
file = %Plug.Upload{
content_type: "image/jpg",
path: Path.absname("test/fixtures/image.jpg"),
filename: "an_image.jpg"
}
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> post("/api/v1/media", %{"file" => file})
assert media = json_response(conn, 200)
assert media["type"] == "image"
end
test "hashtag timeline", %{conn: conn} do
following = insert(:user)
capture_log(fn ->
{:ok, activity} = TwitterAPI.create_status(following, %{"status" => "test #2hu"})
{:ok, [_activity]} =
OStatus.fetch_activity_from_url("https://shitposter.club/notice/2827873")
conn =
conn
|> get("/api/v1/timelines/tag/2hu")
assert [%{"id" => id}] = json_response(conn, 200)
assert id == to_string(activity.id)
end)
end
test "getting followers", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, user} = User.follow(user, other_user)
conn =
conn
|> get("/api/v1/accounts/#{other_user.id}/followers")
assert [%{"id" => id}] = json_response(conn, 200)
assert id == to_string(user.id)
end
test "getting following", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, user} = User.follow(user, other_user)
conn =
conn
|> get("/api/v1/accounts/#{user.id}/following")
assert [%{"id" => id}] = json_response(conn, 200)
assert id == to_string(other_user.id)
end
test "following / unfollowing a user", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
conn =
conn
|> assign(:user, user)
|> post("/api/v1/accounts/#{other_user.id}/follow")
assert %{"id" => _id, "following" => true} = json_response(conn, 200)
user = Repo.get(User, user.id)
conn =
build_conn()
|> assign(:user, user)
|> post("/api/v1/accounts/#{other_user.id}/unfollow")
assert %{"id" => _id, "following" => false} = json_response(conn, 200)
user = Repo.get(User, user.id)
conn =
build_conn()
|> assign(:user, user)
|> post("/api/v1/follows", %{"uri" => other_user.nickname})
assert %{"id" => id} = json_response(conn, 200)
assert id == to_string(other_user.id)
end
test "blocking / unblocking a user", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
conn =
conn
|> assign(:user, user)
|> post("/api/v1/accounts/#{other_user.id}/block")
assert %{"id" => _id, "blocking" => true} = json_response(conn, 200)
user = Repo.get(User, user.id)
conn =
build_conn()
|> assign(:user, user)
|> post("/api/v1/accounts/#{other_user.id}/unblock")
assert %{"id" => _id, "blocking" => false} = json_response(conn, 200)
end
test "getting a list of blocks", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, user} = User.block(user, other_user)
conn =
conn
|> assign(:user, user)
|> get("/api/v1/blocks")
other_user_id = to_string(other_user.id)
assert [%{"id" => ^other_user_id}] = json_response(conn, 200)
end
test "blocking / unblocking a domain", %{conn: conn} do
user = insert(:user)
other_user = insert(:user, %{ap_id: "https://dogwhistle.zone/@pundit"})
conn =
conn
|> assign(:user, user)
|> post("/api/v1/domain_blocks", %{"domain" => "dogwhistle.zone"})
assert %{} = json_response(conn, 200)
user = User.get_cached_by_ap_id(user.ap_id)
assert User.blocks?(user, other_user)
conn =
build_conn()
|> assign(:user, user)
|> delete("/api/v1/domain_blocks", %{"domain" => "dogwhistle.zone"})
assert %{} = json_response(conn, 200)
user = User.get_cached_by_ap_id(user.ap_id)
refute User.blocks?(user, other_user)
end
test "getting a list of domain blocks" do
user = insert(:user)
{:ok, user} = User.block_domain(user, "bad.site")
{:ok, user} = User.block_domain(user, "even.worse.site")
conn =
conn
|> assign(:user, user)
|> get("/api/v1/domain_blocks")
domain_blocks = json_response(conn, 200)
assert "bad.site" in domain_blocks
assert "even.worse.site" in domain_blocks
end
test "unimplemented mute endpoints" do
user = insert(:user)
other_user = insert(:user)
["mute", "unmute"]
|> Enum.each(fn endpoint ->
conn =
build_conn()
|> assign(:user, user)
|> post("/api/v1/accounts/#{other_user.id}/#{endpoint}")
assert %{"id" => id} = json_response(conn, 200)
assert id == to_string(other_user.id)
end)
end
test "unimplemented mutes, follow_requests, blocks, domain blocks" do
user = insert(:user)
["blocks", "domain_blocks", "mutes", "follow_requests"]
|> Enum.each(fn endpoint ->
conn =
build_conn()
|> assign(:user, user)
|> get("/api/v1/#{endpoint}")
assert [] = json_response(conn, 200)
end)
end
test "account search", %{conn: conn} do
user = insert(:user)
user_two = insert(:user, %{nickname: "shp@shitposter.club"})
user_three = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"})
results =
conn
|> assign(:user, user)
|> get("/api/v1/accounts/search", %{"q" => "shp"})
|> json_response(200)
result_ids = for result <- results, do: result["acct"]
assert user_two.nickname in result_ids
assert user_three.nickname in result_ids
results =
conn
|> assign(:user, user)
|> get("/api/v1/accounts/search", %{"q" => "2hu"})
|> json_response(200)
result_ids = for result <- results, do: result["acct"]
assert user_three.nickname in result_ids
end
test "search", %{conn: conn} do
user = insert(:user)
user_two = insert(:user, %{nickname: "shp@shitposter.club"})
user_three = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"})
{:ok, activity} = CommonAPI.post(user, %{"status" => "This is about 2hu"})
{:ok, _activity} =
CommonAPI.post(user, %{
"status" => "This is about 2hu, but private",
"visibility" => "private"
})
{:ok, _} = CommonAPI.post(user_two, %{"status" => "This isn't"})
conn =
conn
|> get("/api/v1/search", %{"q" => "2hu"})
assert results = json_response(conn, 200)
[account | _] = results["accounts"]
assert account["id"] == to_string(user_three.id)
assert results["hashtags"] == []
[status] = results["statuses"]
assert status["id"] == to_string(activity.id)
end
test "search fetches remote statuses", %{conn: conn} do
capture_log(fn ->
conn =
conn
|> get("/api/v1/search", %{"q" => "https://shitposter.club/notice/2827873"})
assert results = json_response(conn, 200)
[status] = results["statuses"]
assert status["uri"] == "tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment"
end)
end
test "search fetches remote accounts", %{conn: conn} do
conn =
conn
|> get("/api/v1/search", %{"q" => "shp@social.heldscal.la", "resolve" => "true"})
assert results = json_response(conn, 200)
[account] = results["accounts"]
assert account["acct"] == "shp@social.heldscal.la"
end
test "returns the favorites of a user", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, _} = CommonAPI.post(other_user, %{"status" => "bla"})
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "traps are happy"})
{:ok, _, _} = CommonAPI.favorite(activity.id, user)
conn =
conn
|> assign(:user, user)
|> get("/api/v1/favourites")
assert [status] = json_response(conn, 200)
assert status["id"] == to_string(activity.id)
end
describe "updating credentials" do
test "updates the user's bio", %{conn: conn} do
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> patch("/api/v1/accounts/update_credentials", %{"note" => "I drink #cofe"})
assert user = json_response(conn, 200)
assert user["note"] == "I drink #cofe"
end
test "updates the user's name", %{conn: conn} do
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> patch("/api/v1/accounts/update_credentials", %{"display_name" => "markorepairs"})
assert user = json_response(conn, 200)
assert user["display_name"] == "markorepairs"
end
test "updates the user's avatar", %{conn: conn} do
user = insert(:user)
new_avatar = %Plug.Upload{
content_type: "image/jpg",
path: Path.absname("test/fixtures/image.jpg"),
filename: "an_image.jpg"
}
conn =
conn
|> assign(:user, user)
|> patch("/api/v1/accounts/update_credentials", %{"avatar" => new_avatar})
assert user = json_response(conn, 200)
assert user["avatar"] != "https://placehold.it/48x48"
end
test "updates the user's banner", %{conn: conn} do
user = insert(:user)
new_header = %Plug.Upload{
content_type: "image/jpg",
path: Path.absname("test/fixtures/image.jpg"),
filename: "an_image.jpg"
}
conn =
conn
|> assign(:user, user)
|> patch("/api/v1/accounts/update_credentials", %{"header" => new_header})
assert user = json_response(conn, 200)
assert user["header"] != "https://placehold.it/700x335"
end
end
test "get instance information", %{conn: conn} do
insert(:user, %{local: true})
user = insert(:user, %{local: true})
insert(:user, %{local: false})
{:ok, _} = TwitterAPI.create_status(user, %{"status" => "cofe"})
Pleroma.Stats.update_stats()
conn =
conn
|> get("/api/v1/instance")
assert result = json_response(conn, 200)
assert result["stats"]["user_count"] == 2
assert result["stats"]["status_count"] == 1
end
end
diff --git a/test/web/twitter_api/twitter_api_controller_test.exs b/test/web/twitter_api/twitter_api_controller_test.exs
index 68f4331df..bd11551df 100644
--- a/test/web/twitter_api/twitter_api_controller_test.exs
+++ b/test/web/twitter_api/twitter_api_controller_test.exs
@@ -1,929 +1,1029 @@
defmodule Pleroma.Web.TwitterAPI.ControllerTest do
use Pleroma.Web.ConnCase
alias Pleroma.Web.TwitterAPI.Representers.ActivityRepresenter
alias Pleroma.Builders.{ActivityBuilder, UserBuilder}
alias Pleroma.{Repo, Activity, User, Object, Notification}
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.TwitterAPI.UserView
alias Pleroma.Web.TwitterAPI.NotificationView
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.TwitterAPI.TwitterAPI
alias Comeonin.Pbkdf2
import Pleroma.Factory
describe "POST /api/account/verify_credentials" do
setup [:valid_user]
test "without valid credentials", %{conn: conn} do
conn = post(conn, "/api/account/verify_credentials.json")
assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
end
test "with credentials", %{conn: conn, user: user} do
conn =
conn
|> with_credentials(user.nickname, "test")
|> post("/api/account/verify_credentials.json")
assert response = json_response(conn, 200)
assert response == UserView.render("show.json", %{user: user, token: response["token"]})
end
end
describe "POST /api/account/most_recent_notification" do
setup [:valid_user]
test "without valid credentials", %{conn: conn} do
conn = post(conn, "/api/account/most_recent_notification.json")
assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
end
test "with credentials", %{conn: conn, user: user} do
conn =
conn
|> with_credentials(user.nickname, "test")
|> post("/api/account/most_recent_notification.json", %{id: "200"})
assert json_response(conn, 200)
user = User.get_by_nickname(user.nickname)
assert user.info["most_recent_notification"] == 200
end
end
describe "POST /statuses/update.json" do
setup [:valid_user]
test "without valid credentials", %{conn: conn} do
conn = post(conn, "/api/statuses/update.json")
assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
end
test "with credentials", %{conn: conn, user: user} do
conn_with_creds = conn |> with_credentials(user.nickname, "test")
request_path = "/api/statuses/update.json"
error_response = %{
"request" => request_path,
"error" => "Client must provide a 'status' parameter with a value."
}
conn = conn_with_creds |> post(request_path)
assert json_response(conn, 400) == error_response
conn = conn_with_creds |> post(request_path, %{status: ""})
assert json_response(conn, 400) == error_response
conn = conn_with_creds |> post(request_path, %{status: " "})
assert json_response(conn, 400) == error_response
conn = conn_with_creds |> post(request_path, %{status: "Nice meme."})
assert json_response(conn, 200) ==
ActivityRepresenter.to_map(Repo.one(Activity), %{user: user})
end
end
describe "GET /statuses/public_timeline.json" do
test "returns statuses", %{conn: conn} do
{:ok, user} = UserBuilder.insert()
activities = ActivityBuilder.insert_list(30, %{}, %{user: user})
ActivityBuilder.insert_list(10, %{}, %{user: user})
since_id = List.last(activities).id
conn =
conn
|> get("/api/statuses/public_timeline.json", %{since_id: since_id})
response = json_response(conn, 200)
assert length(response) == 10
end
end
describe "GET /statuses/show/:id.json" do
test "returns one status", %{conn: conn} do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "Hey!"})
actor = Repo.get_by!(User, ap_id: activity.data["actor"])
conn =
conn
|> get("/api/statuses/show/#{activity.id}.json")
response = json_response(conn, 200)
assert response == ActivityRepresenter.to_map(activity, %{user: actor})
end
end
describe "GET /users/show.json" do
test "gets user with screen_name", %{conn: conn} do
user = insert(:user)
conn =
conn
|> get("/api/users/show.json", %{"screen_name" => user.nickname})
response = json_response(conn, 200)
assert response["id"] == user.id
end
test "gets user with user_id", %{conn: conn} do
user = insert(:user)
conn =
conn
|> get("/api/users/show.json", %{"user_id" => user.id})
response = json_response(conn, 200)
assert response["id"] == user.id
end
test "gets a user for a logged in user", %{conn: conn} do
user = insert(:user)
logged_in = insert(:user)
{:ok, logged_in, user, _activity} = TwitterAPI.follow(logged_in, %{"user_id" => user.id})
conn =
conn
|> with_credentials(logged_in.nickname, "test")
|> get("/api/users/show.json", %{"user_id" => user.id})
response = json_response(conn, 200)
assert response["following"] == true
end
end
describe "GET /statusnet/conversation/:id.json" do
test "returns the statuses in the conversation", %{conn: conn} do
{:ok, _user} = UserBuilder.insert()
{:ok, activity} = ActivityBuilder.insert(%{"type" => "Create", "context" => "2hu"})
{:ok, _activity_two} = ActivityBuilder.insert(%{"type" => "Create", "context" => "2hu"})
{:ok, _activity_three} = ActivityBuilder.insert(%{"type" => "Create", "context" => "3hu"})
conn =
conn
|> get("/api/statusnet/conversation/#{activity.data["context_id"]}.json")
response = json_response(conn, 200)
assert length(response) == 2
end
end
describe "GET /statuses/friends_timeline.json" do
setup [:valid_user]
test "without valid credentials", %{conn: conn} do
conn = get(conn, "/api/statuses/friends_timeline.json")
assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
end
test "with credentials", %{conn: conn, user: current_user} do
user = insert(:user)
activities =
ActivityBuilder.insert_list(30, %{"to" => [User.ap_followers(user)]}, %{user: user})
returned_activities =
ActivityBuilder.insert_list(10, %{"to" => [User.ap_followers(user)]}, %{user: user})
other_user = insert(:user)
ActivityBuilder.insert_list(10, %{}, %{user: other_user})
since_id = List.last(activities).id
current_user =
Ecto.Changeset.change(current_user, following: [User.ap_followers(user)])
|> Repo.update!()
conn =
conn
|> with_credentials(current_user.nickname, "test")
|> get("/api/statuses/friends_timeline.json", %{since_id: since_id})
response = json_response(conn, 200)
assert length(response) == 10
assert response ==
Enum.map(returned_activities, fn activity ->
ActivityRepresenter.to_map(activity, %{
user: User.get_cached_by_ap_id(activity.data["actor"]),
for: current_user
})
end)
end
end
describe "GET /statuses/mentions.json" do
setup [:valid_user]
test "without valid credentials", %{conn: conn} do
conn = get(conn, "/api/statuses/mentions.json")
assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
end
test "with credentials", %{conn: conn, user: current_user} do
{:ok, activity} =
ActivityBuilder.insert(%{"to" => [current_user.ap_id]}, %{user: current_user})
conn =
conn
|> with_credentials(current_user.nickname, "test")
|> get("/api/statuses/mentions.json")
response = json_response(conn, 200)
assert length(response) == 1
assert Enum.at(response, 0) ==
ActivityRepresenter.to_map(activity, %{
user: current_user,
mentioned: [current_user]
})
end
end
describe "GET /api/qvitter/statuses/notifications.json" do
setup [:valid_user]
test "without valid credentials", %{conn: conn} do
conn = get(conn, "/api/qvitter/statuses/notifications.json")
assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
end
test "with credentials", %{conn: conn, user: current_user} do
other_user = insert(:user)
{:ok, _activity} =
ActivityBuilder.insert(%{"to" => [current_user.ap_id]}, %{user: other_user})
conn =
conn
|> with_credentials(current_user.nickname, "test")
|> get("/api/qvitter/statuses/notifications.json")
response = json_response(conn, 200)
assert length(response) == 1
assert response ==
NotificationView.render("notification.json", %{
notifications: Notification.for_user(current_user),
for: current_user
})
end
end
describe "GET /statuses/user_timeline.json" do
setup [:valid_user]
test "without any params", %{conn: conn} do
conn = get(conn, "/api/statuses/user_timeline.json")
assert json_response(conn, 400) == %{
"error" => "You need to specify screen_name or user_id",
"request" => "/api/statuses/user_timeline.json"
}
end
test "with user_id", %{conn: conn} do
user = insert(:user)
{:ok, activity} = ActivityBuilder.insert(%{"id" => 1}, %{user: user})
conn = get(conn, "/api/statuses/user_timeline.json", %{"user_id" => user.id})
response = json_response(conn, 200)
assert length(response) == 1
assert Enum.at(response, 0) == ActivityRepresenter.to_map(activity, %{user: user})
end
test "with screen_name", %{conn: conn} do
user = insert(:user)
{:ok, activity} = ActivityBuilder.insert(%{"id" => 1}, %{user: user})
conn = get(conn, "/api/statuses/user_timeline.json", %{"screen_name" => user.nickname})
response = json_response(conn, 200)
assert length(response) == 1
assert Enum.at(response, 0) == ActivityRepresenter.to_map(activity, %{user: user})
end
test "with credentials", %{conn: conn, user: current_user} do
{:ok, activity} = ActivityBuilder.insert(%{"id" => 1}, %{user: current_user})
conn =
conn
|> with_credentials(current_user.nickname, "test")
|> get("/api/statuses/user_timeline.json")
response = json_response(conn, 200)
assert length(response) == 1
assert Enum.at(response, 0) == ActivityRepresenter.to_map(activity, %{user: current_user})
end
test "with credentials with user_id", %{conn: conn, user: current_user} do
user = insert(:user)
{:ok, activity} = ActivityBuilder.insert(%{"id" => 1}, %{user: user})
conn =
conn
|> with_credentials(current_user.nickname, "test")
|> get("/api/statuses/user_timeline.json", %{"user_id" => user.id})
response = json_response(conn, 200)
assert length(response) == 1
assert Enum.at(response, 0) == ActivityRepresenter.to_map(activity, %{user: user})
end
test "with credentials screen_name", %{conn: conn, user: current_user} do
user = insert(:user)
{:ok, activity} = ActivityBuilder.insert(%{"id" => 1}, %{user: user})
conn =
conn
|> with_credentials(current_user.nickname, "test")
|> get("/api/statuses/user_timeline.json", %{"screen_name" => user.nickname})
response = json_response(conn, 200)
assert length(response) == 1
assert Enum.at(response, 0) == ActivityRepresenter.to_map(activity, %{user: user})
end
end
describe "POST /friendships/create.json" do
setup [:valid_user]
test "without valid credentials", %{conn: conn} do
conn = post(conn, "/api/friendships/create.json")
assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
end
test "with credentials", %{conn: conn, user: current_user} do
followed = insert(:user)
conn =
conn
|> with_credentials(current_user.nickname, "test")
|> post("/api/friendships/create.json", %{user_id: followed.id})
current_user = Repo.get(User, current_user.id)
assert User.ap_followers(followed) in current_user.following
assert json_response(conn, 200) ==
UserView.render("show.json", %{user: followed, for: current_user})
end
end
describe "POST /friendships/destroy.json" do
setup [:valid_user]
test "without valid credentials", %{conn: conn} do
conn = post(conn, "/api/friendships/destroy.json")
assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
end
test "with credentials", %{conn: conn, user: current_user} do
followed = insert(:user)
{:ok, current_user} = User.follow(current_user, followed)
assert User.ap_followers(followed) in current_user.following
ActivityPub.follow(current_user, followed)
conn =
conn
|> with_credentials(current_user.nickname, "test")
|> post("/api/friendships/destroy.json", %{user_id: followed.id})
current_user = Repo.get(User, current_user.id)
assert current_user.following == [current_user.ap_id]
assert json_response(conn, 200) ==
UserView.render("show.json", %{user: followed, for: current_user})
end
end
describe "POST /blocks/create.json" do
setup [:valid_user]
test "without valid credentials", %{conn: conn} do
conn = post(conn, "/api/blocks/create.json")
assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
end
test "with credentials", %{conn: conn, user: current_user} do
blocked = insert(:user)
conn =
conn
|> with_credentials(current_user.nickname, "test")
|> post("/api/blocks/create.json", %{user_id: blocked.id})
current_user = Repo.get(User, current_user.id)
assert User.blocks?(current_user, blocked)
assert json_response(conn, 200) ==
UserView.render("show.json", %{user: blocked, for: current_user})
end
end
describe "POST /blocks/destroy.json" do
setup [:valid_user]
test "without valid credentials", %{conn: conn} do
conn = post(conn, "/api/blocks/destroy.json")
assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
end
test "with credentials", %{conn: conn, user: current_user} do
blocked = insert(:user)
{:ok, current_user, blocked} = TwitterAPI.block(current_user, %{"user_id" => blocked.id})
assert User.blocks?(current_user, blocked)
conn =
conn
|> with_credentials(current_user.nickname, "test")
|> post("/api/blocks/destroy.json", %{user_id: blocked.id})
current_user = Repo.get(User, current_user.id)
assert current_user.info["blocks"] == []
assert json_response(conn, 200) ==
UserView.render("show.json", %{user: blocked, for: current_user})
end
end
describe "GET /help/test.json" do
test "returns \"ok\"", %{conn: conn} do
conn = get(conn, "/api/help/test.json")
assert json_response(conn, 200) == "ok"
end
end
describe "POST /api/qvitter/update_avatar.json" do
setup [:valid_user]
test "without valid credentials", %{conn: conn} do
conn = post(conn, "/api/qvitter/update_avatar.json")
assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
end
test "with credentials", %{conn: conn, user: current_user} do
avatar_image = File.read!("test/fixtures/avatar_data_uri")
conn =
conn
|> with_credentials(current_user.nickname, "test")
|> post("/api/qvitter/update_avatar.json", %{img: avatar_image})
current_user = Repo.get(User, current_user.id)
assert is_map(current_user.avatar)
assert json_response(conn, 200) ==
UserView.render("show.json", %{user: current_user, for: current_user})
end
end
describe "POST /api/favorites/create/:id" do
setup [:valid_user]
test "without valid credentials", %{conn: conn} do
note_activity = insert(:note_activity)
conn = post(conn, "/api/favorites/create/#{note_activity.id}.json")
assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
end
test "with credentials", %{conn: conn, user: current_user} do
note_activity = insert(:note_activity)
conn =
conn
|> with_credentials(current_user.nickname, "test")
|> post("/api/favorites/create/#{note_activity.id}.json")
assert json_response(conn, 200)
end
test "with credentials, invalid param", %{conn: conn, user: current_user} do
conn =
conn
|> with_credentials(current_user.nickname, "test")
|> post("/api/favorites/create/wrong.json")
assert json_response(conn, 400)
end
test "with credentials, invalid activity", %{conn: conn, user: current_user} do
conn =
conn
|> with_credentials(current_user.nickname, "test")
|> post("/api/favorites/create/1.json")
assert json_response(conn, 500)
end
end
describe "POST /api/favorites/destroy/:id" do
setup [:valid_user]
test "without valid credentials", %{conn: conn} do
note_activity = insert(:note_activity)
conn = post(conn, "/api/favorites/destroy/#{note_activity.id}.json")
assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
end
test "with credentials", %{conn: conn, user: current_user} do
note_activity = insert(:note_activity)
object = Object.get_by_ap_id(note_activity.data["object"]["id"])
ActivityPub.like(current_user, object)
conn =
conn
|> with_credentials(current_user.nickname, "test")
|> post("/api/favorites/destroy/#{note_activity.id}.json")
assert json_response(conn, 200)
end
end
describe "POST /api/statuses/retweet/:id" do
setup [:valid_user]
test "without valid credentials", %{conn: conn} do
note_activity = insert(:note_activity)
conn = post(conn, "/api/statuses/retweet/#{note_activity.id}.json")
assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
end
test "with credentials", %{conn: conn, user: current_user} do
note_activity = insert(:note_activity)
request_path = "/api/statuses/retweet/#{note_activity.id}.json"
response =
conn
|> with_credentials(current_user.nickname, "test")
|> post(request_path)
activity = Repo.get(Activity, note_activity.id)
activity_user = Repo.get_by(User, ap_id: note_activity.data["actor"])
assert json_response(response, 200) ==
ActivityRepresenter.to_map(activity, %{user: activity_user, for: current_user})
end
end
describe "POST /api/account/register" do
test "it creates a new user", %{conn: conn} do
data = %{
"nickname" => "lain",
"email" => "lain@wired.jp",
"fullname" => "lain iwakura",
"bio" => "close the world.",
"password" => "bear",
"confirm" => "bear"
}
conn =
conn
|> post("/api/account/register", data)
user = json_response(conn, 200)
fetched_user = Repo.get_by(User, nickname: "lain")
assert user == UserView.render("show.json", %{user: fetched_user})
end
test "it returns errors on a problem", %{conn: conn} do
data = %{
"email" => "lain@wired.jp",
"fullname" => "lain iwakura",
"bio" => "close the world.",
"password" => "bear",
"confirm" => "bear"
}
conn =
conn
|> post("/api/account/register", data)
errors = json_response(conn, 400)
assert is_binary(errors["error"])
end
end
describe "GET /api/externalprofile/show" do
test "it returns the user", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
conn =
conn
|> assign(:user, user)
|> get("/api/externalprofile/show", %{profileurl: other_user.ap_id})
assert json_response(conn, 200) == UserView.render("show.json", %{user: other_user})
end
end
describe "GET /api/statuses/followers" do
test "it returns a user's followers", %{conn: conn} do
user = insert(:user)
follower_one = insert(:user)
follower_two = insert(:user)
_not_follower = insert(:user)
{:ok, follower_one} = User.follow(follower_one, user)
{:ok, follower_two} = User.follow(follower_two, user)
conn =
conn
|> assign(:user, user)
|> get("/api/statuses/followers")
expected = UserView.render("index.json", %{users: [follower_one, follower_two], for: user})
result = json_response(conn, 200)
assert Enum.sort(expected) == Enum.sort(result)
end
end
describe "GET /api/statuses/friends" do
test "it returns the logged in user's friends", %{conn: conn} do
user = insert(:user)
followed_one = insert(:user)
followed_two = insert(:user)
_not_followed = insert(:user)
{:ok, user} = User.follow(user, followed_one)
{:ok, user} = User.follow(user, followed_two)
conn =
conn
|> assign(:user, user)
|> get("/api/statuses/friends")
expected = UserView.render("index.json", %{users: [followed_one, followed_two], for: user})
result = json_response(conn, 200)
assert Enum.sort(expected) == Enum.sort(result)
end
test "it returns a given user's friends with user_id", %{conn: conn} do
user = insert(:user)
followed_one = insert(:user)
followed_two = insert(:user)
_not_followed = insert(:user)
{:ok, user} = User.follow(user, followed_one)
{:ok, user} = User.follow(user, followed_two)
conn =
conn
|> assign(:user, user)
|> get("/api/statuses/friends", %{"user_id" => user.id})
assert MapSet.equal?(
MapSet.new(json_response(conn, 200)),
MapSet.new(
UserView.render("index.json", %{users: [followed_one, followed_two], for: user})
)
)
end
test "it returns a given user's friends with screen_name", %{conn: conn} do
user = insert(:user)
followed_one = insert(:user)
followed_two = insert(:user)
_not_followed = insert(:user)
{:ok, user} = User.follow(user, followed_one)
{:ok, user} = User.follow(user, followed_two)
conn =
conn
|> assign(:user, user)
|> get("/api/statuses/friends", %{"screen_name" => user.nickname})
assert MapSet.equal?(
MapSet.new(json_response(conn, 200)),
MapSet.new(
UserView.render("index.json", %{users: [followed_one, followed_two], for: user})
)
)
end
end
describe "GET /friends/ids" do
test "it returns a user's friends", %{conn: conn} do
user = insert(:user)
followed_one = insert(:user)
followed_two = insert(:user)
_not_followed = insert(:user)
{:ok, user} = User.follow(user, followed_one)
{:ok, user} = User.follow(user, followed_two)
conn =
conn
|> assign(:user, user)
|> get("/api/friends/ids")
expected = [followed_one.id, followed_two.id]
assert MapSet.equal?(
MapSet.new(Poison.decode!(json_response(conn, 200))),
MapSet.new(expected)
)
end
end
describe "POST /api/account/update_profile.json" do
test "it updates a user's profile", %{conn: conn} do
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> post("/api/account/update_profile.json", %{
"name" => "new name",
"description" => "new description"
})
user = Repo.get!(User, user.id)
assert user.name == "new name"
assert user.bio == "new description"
assert json_response(conn, 200) == UserView.render("user.json", %{user: user, for: user})
end
+
+ test "it locks an account", %{conn: conn} do
+ user = insert(:user)
+
+ conn =
+ conn
+ |> assign(:user, user)
+ |> post("/api/account/update_profile.json", %{
+ "locked" => "true"
+ })
+
+ user = Repo.get!(User, user.id)
+ assert user.info["locked"] == true
+
+ assert json_response(conn, 200) == UserView.render("user.json", %{user: user, for: user})
+ end
+
+ test "it unlocks an account", %{conn: conn} do
+ user = insert(:user)
+
+ conn =
+ conn
+ |> assign(:user, user)
+ |> post("/api/account/update_profile.json", %{
+ "locked" => "false"
+ })
+
+ user = Repo.get!(User, user.id)
+ assert user.info["locked"] == false
+
+ assert json_response(conn, 200) == UserView.render("user.json", %{user: user, for: user})
+ end
end
defp valid_user(_context) do
user = insert(:user)
[user: user]
end
defp with_credentials(conn, username, password) do
header_content = "Basic " <> Base.encode64("#{username}:#{password}")
put_req_header(conn, "authorization", header_content)
end
describe "GET /api/search.json" do
test "it returns search results", %{conn: conn} do
user = insert(:user)
user_two = insert(:user, %{nickname: "shp@shitposter.club"})
{:ok, activity} = CommonAPI.post(user, %{"status" => "This is about 2hu"})
{:ok, _} = CommonAPI.post(user_two, %{"status" => "This isn't"})
conn =
conn
|> get("/api/search.json", %{"q" => "2hu", "page" => "1", "rpp" => "1"})
assert [status] = json_response(conn, 200)
assert status["id"] == activity.id
end
end
describe "GET /api/statusnet/tags/timeline/:tag.json" do
test "it returns the tags timeline", %{conn: conn} do
user = insert(:user)
user_two = insert(:user, %{nickname: "shp@shitposter.club"})
{:ok, activity} = CommonAPI.post(user, %{"status" => "This is about #2hu"})
{:ok, _} = CommonAPI.post(user_two, %{"status" => "This isn't"})
conn =
conn
|> get("/api/statusnet/tags/timeline/2hu.json")
assert [status] = json_response(conn, 200)
assert status["id"] == activity.id
end
end
test "Convert newlines to <br> in bio", %{conn: conn} do
user = insert(:user)
_conn =
conn
|> assign(:user, user)
|> post("/api/account/update_profile.json", %{
"description" => "Hello,\r\nWorld! I\n am a test."
})
user = Repo.get!(User, user.id)
assert user.bio == "Hello,<br>World! I<br> am a test."
end
describe "POST /api/pleroma/change_password" do
setup [:valid_user]
test "without credentials", %{conn: conn} do
conn = post(conn, "/api/pleroma/change_password")
assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
end
test "with credentials and invalid password", %{conn: conn, user: current_user} do
conn =
conn
|> with_credentials(current_user.nickname, "test")
|> post("/api/pleroma/change_password", %{
"password" => "hi",
"new_password" => "newpass",
"new_password_confirmation" => "newpass"
})
assert json_response(conn, 200) == %{"error" => "Invalid password."}
end
test "with credentials, valid password and new password and confirmation not matching", %{
conn: conn,
user: current_user
} do
conn =
conn
|> with_credentials(current_user.nickname, "test")
|> post("/api/pleroma/change_password", %{
"password" => "test",
"new_password" => "newpass",
"new_password_confirmation" => "notnewpass"
})
assert json_response(conn, 200) == %{
"error" => "New password does not match confirmation."
}
end
test "with credentials, valid password and invalid new password", %{
conn: conn,
user: current_user
} do
conn =
conn
|> with_credentials(current_user.nickname, "test")
|> post("/api/pleroma/change_password", %{
"password" => "test",
"new_password" => "",
"new_password_confirmation" => ""
})
assert json_response(conn, 200) == %{
"error" => "New password can't be blank."
}
end
test "with credentials, valid password and matching new password and confirmation", %{
conn: conn,
user: current_user
} do
conn =
conn
|> with_credentials(current_user.nickname, "test")
|> post("/api/pleroma/change_password", %{
"password" => "test",
"new_password" => "newpass",
"new_password_confirmation" => "newpass"
})
assert json_response(conn, 200) == %{"status" => "success"}
fetched_user = Repo.get(User, current_user.id)
assert Pbkdf2.checkpw("newpass", fetched_user.password_hash) == true
end
end
describe "POST /api/pleroma/delete_account" do
setup [:valid_user]
test "without credentials", %{conn: conn} do
conn = post(conn, "/api/pleroma/delete_account")
assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
end
test "with credentials and invalid password", %{conn: conn, user: current_user} do
conn =
conn
|> with_credentials(current_user.nickname, "test")
|> post("/api/pleroma/delete_account", %{"password" => "hi"})
assert json_response(conn, 200) == %{"error" => "Invalid password."}
end
test "with credentials and valid password", %{conn: conn, user: current_user} do
conn =
conn
|> with_credentials(current_user.nickname, "test")
|> post("/api/pleroma/delete_account", %{"password" => "test"})
assert json_response(conn, 200) == %{"status" => "success"}
# Wait a second for the started task to end
:timer.sleep(1000)
end
end
+
+ describe "GET /api/pleroma/friend_requests" do
+ test "it lists friend requests" do
+ user = insert(:user, %{info: %{"locked" => true}})
+ other_user = insert(:user)
+
+ {:ok, activity} = ActivityPub.follow(other_user, user)
+
+ user = Repo.get(User, user.id)
+ other_user = Repo.get(User, other_user.id)
+
+ assert User.following?(other_user, user) == false
+
+ conn =
+ build_conn()
+ |> assign(:user, user)
+ |> get("/api/pleroma/friend_requests")
+
+ assert [relationship] = json_response(conn, 200)
+ assert other_user.id == relationship["id"]
+ end
+ end
+
+ describe "POST /api/pleroma/friendships/approve" do
+ test "it approves a friend request" do
+ user = insert(:user, %{info: %{"locked" => true}})
+ other_user = insert(:user)
+
+ {:ok, activity} = ActivityPub.follow(other_user, user)
+
+ user = Repo.get(User, user.id)
+ other_user = Repo.get(User, other_user.id)
+
+ assert User.following?(other_user, user) == false
+
+ conn =
+ build_conn()
+ |> assign(:user, user)
+ |> post("/api/pleroma/friendships/approve", %{"user_id" => to_string(other_user.id)})
+
+ assert relationship = json_response(conn, 200)
+ assert other_user.id == relationship["id"]
+ assert relationship["follows_you"] == true
+ end
+ end
+
+ describe "POST /api/pleroma/friendships/deny" do
+ test "it denies a friend request" do
+ user = insert(:user, %{info: %{"locked" => true}})
+ other_user = insert(:user)
+
+ {:ok, activity} = ActivityPub.follow(other_user, user)
+
+ user = Repo.get(User, user.id)
+ other_user = Repo.get(User, other_user.id)
+
+ assert User.following?(other_user, user) == false
+
+ conn =
+ build_conn()
+ |> assign(:user, user)
+ |> post("/api/pleroma/friendships/deny", %{"user_id" => to_string(other_user.id)})
+
+ assert relationship = json_response(conn, 200)
+ assert other_user.id == relationship["id"]
+ assert relationship["follows_you"] == false
+ end
+ end
end
diff --git a/test/web/twitter_api/views/user_view_test.exs b/test/web/twitter_api/views/user_view_test.exs
index 9f8bf4cdc..eea743b32 100644
--- a/test/web/twitter_api/views/user_view_test.exs
+++ b/test/web/twitter_api/views/user_view_test.exs
@@ -1,182 +1,186 @@
defmodule Pleroma.Web.TwitterAPI.UserViewTest do
use Pleroma.DataCase
alias Pleroma.User
alias Pleroma.Web.TwitterAPI.UserView
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Builders.UserBuilder
import Pleroma.Factory
setup do
user = insert(:user, bio: "<span>Here's some html</span>")
[user: user]
end
test "A user with an avatar object", %{user: user} do
image = "image"
user = %{user | avatar: %{"url" => [%{"href" => image}]}}
represented = UserView.render("show.json", %{user: user})
assert represented["profile_image_url"] == image
end
test "A user" do
note_activity = insert(:note_activity)
user = User.get_cached_by_ap_id(note_activity.data["actor"])
{:ok, user} = User.update_note_count(user)
follower = insert(:user)
second_follower = insert(:user)
User.follow(follower, user)
User.follow(second_follower, user)
User.follow(user, follower)
{:ok, user} = User.update_follower_count(user)
Cachex.put(:user_cache, "user_info:#{user.id}", User.user_info(Repo.get!(User, user.id)))
image = "http://localhost:4001/images/avi.png"
banner = "http://localhost:4001/images/banner.png"
represented = %{
"id" => user.id,
"name" => user.name,
"screen_name" => user.nickname,
"description" => HtmlSanitizeEx.strip_tags(user.bio),
"created_at" => user.inserted_at |> Utils.format_naive_asctime(),
"favourites_count" => 0,
"statuses_count" => 1,
"friends_count" => 1,
"followers_count" => 2,
"profile_image_url" => image,
"profile_image_url_https" => image,
"profile_image_url_profile_size" => image,
"profile_image_url_original" => image,
"following" => false,
"follows_you" => false,
"statusnet_blocking" => false,
"rights" => %{
"delete_others_notice" => false
},
"statusnet_profile_url" => user.ap_id,
"cover_photo" => banner,
"background_image" => nil,
- "is_local" => true
+ "is_local" => true,
+ "locked" => false
}
assert represented == UserView.render("show.json", %{user: user})
end
test "A user for a given other follower", %{user: user} do
{:ok, follower} = UserBuilder.insert(%{following: [User.ap_followers(user)]})
{:ok, user} = User.update_follower_count(user)
image = "http://localhost:4001/images/avi.png"
banner = "http://localhost:4001/images/banner.png"
represented = %{
"id" => user.id,
"name" => user.name,
"screen_name" => user.nickname,
"description" => HtmlSanitizeEx.strip_tags(user.bio),
"created_at" => user.inserted_at |> Utils.format_naive_asctime(),
"favourites_count" => 0,
"statuses_count" => 0,
"friends_count" => 0,
"followers_count" => 1,
"profile_image_url" => image,
"profile_image_url_https" => image,
"profile_image_url_profile_size" => image,
"profile_image_url_original" => image,
"following" => true,
"follows_you" => false,
"statusnet_blocking" => false,
"rights" => %{
"delete_others_notice" => false
},
"statusnet_profile_url" => user.ap_id,
"cover_photo" => banner,
"background_image" => nil,
- "is_local" => true
+ "is_local" => true,
+ "locked" => false
}
assert represented == UserView.render("show.json", %{user: user, for: follower})
end
test "A user that follows you", %{user: user} do
follower = insert(:user)
{:ok, follower} = User.follow(follower, user)
{:ok, user} = User.update_follower_count(user)
image = "http://localhost:4001/images/avi.png"
banner = "http://localhost:4001/images/banner.png"
represented = %{
"id" => follower.id,
"name" => follower.name,
"screen_name" => follower.nickname,
"description" => HtmlSanitizeEx.strip_tags(follower.bio),
"created_at" => follower.inserted_at |> Utils.format_naive_asctime(),
"favourites_count" => 0,
"statuses_count" => 0,
"friends_count" => 1,
"followers_count" => 0,
"profile_image_url" => image,
"profile_image_url_https" => image,
"profile_image_url_profile_size" => image,
"profile_image_url_original" => image,
"following" => false,
"follows_you" => true,
"statusnet_blocking" => false,
"rights" => %{
"delete_others_notice" => false
},
"statusnet_profile_url" => follower.ap_id,
"cover_photo" => banner,
"background_image" => nil,
- "is_local" => true
+ "is_local" => true,
+ "locked" => false
}
assert represented == UserView.render("show.json", %{user: follower, for: user})
end
test "a user that is a moderator" do
user = insert(:user, %{info: %{"is_moderator" => true}})
represented = UserView.render("show.json", %{user: user, for: user})
assert represented["rights"]["delete_others_notice"]
end
test "A blocked user for the blocker" do
user = insert(:user)
blocker = insert(:user)
User.block(blocker, user)
image = "http://localhost:4001/images/avi.png"
banner = "http://localhost:4001/images/banner.png"
represented = %{
"id" => user.id,
"name" => user.name,
"screen_name" => user.nickname,
"description" => HtmlSanitizeEx.strip_tags(user.bio),
"created_at" => user.inserted_at |> Utils.format_naive_asctime(),
"favourites_count" => 0,
"statuses_count" => 0,
"friends_count" => 0,
"followers_count" => 0,
"profile_image_url" => image,
"profile_image_url_https" => image,
"profile_image_url_profile_size" => image,
"profile_image_url_original" => image,
"following" => false,
"follows_you" => false,
"statusnet_blocking" => true,
"rights" => %{
"delete_others_notice" => false
},
"statusnet_profile_url" => user.ap_id,
"cover_photo" => banner,
"background_image" => nil,
- "is_local" => true
+ "is_local" => true,
+ "locked" => false
}
blocker = Repo.get(User, blocker.id)
assert represented == UserView.render("show.json", %{user: user, for: blocker})
end
end
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sat, Sep 19, 12:26 AM (17 h, 33 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1768548
Default Alt Text
(207 KB)
Attached To
Mode
rPUBE pleroma-upstream
Attached
Detach File
Event Timeline
Log In to Comment