Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85803873
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
61 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index 399a66787..6a8129ac8 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -1,554 +1,558 @@
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
}
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 follow(%User{} = follower, %User{info: info} = followed) do
ap_followers = followed.follower_address
if following?(follower, followed) or info["deactivated"] do
{:error, "Could not follow user: #{followed.nickname} is already on your list."}
else
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 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.set(:user_cache, "ap_id:#{user.ap_id}", user)
Cachex.set(:user_cache, "nickname:#{user.nickname}", user)
Cachex.set(: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.get!(:user_cache, key, fallback: fn _ -> get_by_ap_id(ap_id) end)
end
def get_cached_by_nickname(nickname) do
key = "nickname:#{nickname}"
Cachex.get!(:user_cache, key, fallback: 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.get!(:user_cache, key, fallback: 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 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
- )}
+ search_distance:
+ fragment(
+ "? <-> (? || ?)",
+ ^query,
+ u.nickname,
+ u.name
+ )
+ }
)
- q = from(s in subquery(inner),
- order_by: s.search_distance,
- limit: 20
- )
+ 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"] || []
Enum.member?(blocks, ap_id)
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/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex
index 0d539c02c..fe0294c5c 100644
--- a/lib/pleroma/web/activity_pub/transmogrifier.ex
+++ b/lib/pleroma/web/activity_pub/transmogrifier.ex
@@ -1,534 +1,537 @@
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
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})
User.follow(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"]
update_data =
new_user_data
|> Map.take([:name, :bio, :avatar])
|> Map.put(:info, Map.merge(actor.info, %{"banner" => banner}))
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 =
case object_id do
%{"id" => id} -> id
id -> id
end
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, false) do
User.unfollow(follower, followed)
{:ok, activity}
else
e -> :error
end
end
def handle_incoming(%{"type" => "Undo", "object" => object_id} = data) do
object = Object.get_by_ap_id(object_id).data
data = Map.put(data, "object", object)
handle_incoming(data)
end
- def handle_incoming(%{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = data) do
+ def handle_incoming(
+ %{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = data
+ ) do
with %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, false) do
User.unfollow(blocker, blocked)
User.block(blocker, blocked.ap_id)
{:ok, activity}
else
e -> :error
end
end
+
# TODO
# Accept
# Undo for non-Announce
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
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
end
diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex
index c29fc6b07..846dd97c2 100644
--- a/lib/pleroma/web/activity_pub/utils.ex
+++ b/lib/pleroma/web/activity_pub/utils.ex
@@ -1,375 +1,376 @@
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
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 """
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
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
end
def fetch_latest_follow(%User{ap_id: follower_id}, %User{ap_id: followed_id}) do
query =
from(
activity in Activity,
where:
fragment(
"? @> ?",
activity.data,
^%{type: "Follow", actor: follower_id, 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: 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' = '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 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) do
%{
"type" => "Undo",
"actor" => follower.ap_id,
"to" => [followed.ap_id],
"object" => follow_activity.data["id"]
}
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(
"? @> ?",
activity.data,
^%{type: "Block", actor: blocker_id, object: blocked_id}
),
order_by: [desc: :id],
limit: 1
)
Repo.one(query)
end
def make_block_data(blocker, blocked) do
%{
"type" => "Block",
"actor" => blocker.ap_id,
"to" => [blocked.ap_id],
"object" => blocked.ap_id
}
end
def make_unblock_data(blocker, blocked, block_activity) do
%{
"type" => "Undo",
"actor" => blocker.ap_id,
"to" => [blocked.ap_id],
"object" => block_activity.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/test/fixtures/mastodon-block-activity.json b/test/fixtures/mastodon-block-activity.json
new file mode 100644
index 000000000..186719d2c
--- /dev/null
+++ b/test/fixtures/mastodon-block-activity.json
@@ -0,0 +1,29 @@
+{
+ "type": "Block",
+ "signature": {
+ "type": "RsaSignature2017",
+ "signatureValue": "Kn1/UkAQGJVaXBfWLAHcnwHg8YMAUqlEaBuYLazAG+pz5hqivsyrBmPV186Xzr+B4ZLExA9+SnOoNx/GOz4hBm0kAmukNSILAsUd84tcJ2yT9zc1RKtembK4WiwOw7li0+maeDN0HaB6t+6eTqsCWmtiZpprhXD8V1GGT8yG7X24fQ9oFGn+ng7lasbcCC0988Y1eGqNe7KryxcPuQz57YkDapvtONzk8gyLTkZMV4De93MyRHq6GVjQVIgtiYabQAxrX6Q8C+4P/jQoqdWJHEe+MY5JKyNaT/hMPt2Md1ok9fZQBGHlErk22/zy8bSN19GdG09HmIysBUHRYpBLig==",
+ "creator": "http://mastodon.example.org/users/admin#main-key",
+ "created": "2018-02-17T13:29:31Z"
+ },
+ "object": "http://localtesting.pleroma.lol/users/lain",
+ "nickname": "lain",
+ "id": "http://mastodon.example.org/users/admin#follows/2",
+ "actor": "http://mastodon.example.org/users/admin",
+ "@context": [
+ "https://www.w3.org/ns/activitystreams",
+ "https://w3id.org/security/v1",
+ {
+ "toot": "http://joinmastodon.org/ns#",
+ "sensitive": "as:sensitive",
+ "ostatus": "http://ostatus.org#",
+ "movedTo": "as:movedTo",
+ "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
+ "inReplyToAtomUri": "ostatus:inReplyToAtomUri",
+ "conversation": "ostatus:conversation",
+ "atomUri": "ostatus:atomUri",
+ "Hashtag": "as:Hashtag",
+ "Emoji": "toot:Emoji"
+ }
+ ]
+}
diff --git a/test/fixtures/mastodon-unblock-activity.json b/test/fixtures/mastodon-unblock-activity.json
new file mode 100644
index 000000000..53b2f42a8
--- /dev/null
+++ b/test/fixtures/mastodon-unblock-activity.json
@@ -0,0 +1,33 @@
+{
+ "@context": [
+ "https://www.w3.org/ns/activitystreams",
+ "https://w3id.org/security/v1",
+ {
+ "toot": "http://joinmastodon.org/ns#",
+ "sensitive": "as:sensitive",
+ "ostatus": "http://ostatus.org#",
+ "movedTo": "as:movedTo",
+ "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
+ "inReplyToAtomUri": "ostatus:inReplyToAtomUri",
+ "conversation": "ostatus:conversation",
+ "atomUri": "ostatus:atomUri",
+ "Hashtag": "as:Hashtag",
+ "Emoji": "toot:Emoji"
+ }
+ ],
+ "signature": {
+ "type": "RsaSignature2017",
+ "signatureValue": "Kn1/UkAQGJVaXBfWLAHcnwHg8YMAUqlEaBuYLazAG+pz5hqivsyrBmPV186Xzr+B4ZLExA9+SnOoNx/GOz4hBm0kAmukNSILAsUd84tcJ2yT9zc1RKtembK4WiwOw7li0+maeDN0HaB6t+6eTqsCWmtiZpprhXD8V1GGT8yG7X24fQ9oFGn+ng7lasbcCC0988Y1eGqNe7KryxcPuQz57YkDapvtONzk8gyLTkZMV4De93MyRHq6GVjQVIgtiYabQAxrX6Q8C+4P/jQoqdWJHEe+MY5JKyNaT/hMPt2Md1ok9fZQBGHlErk22/zy8bSN19GdG09HmIysBUHRYpBLig==",
+ "creator": "http://mastodon.example.org/users/admin#main-key",
+ "created": "2018-02-17T13:29:31Z"
+ },
+ "type": "Undo",
+ "object": {
+ "type": "Block",
+ "object": "http://localtesting.pleroma.lol/users/lain",
+ "nickname": "lain",
+ "id": "http://mastodon.example.org/users/admin#follows/2",
+ "actor": "http://mastodon.example.org/users/admin"
+ },
+ "actor": "http://mastodon.example.org/users/admin"
+}
diff --git a/test/web/activity_pub/transmogrifier_test.exs b/test/web/activity_pub/transmogrifier_test.exs
index 8ed7147f4..0ca00a07e 100644
--- a/test/web/activity_pub/transmogrifier_test.exs
+++ b/test/web/activity_pub/transmogrifier_test.exs
@@ -1,447 +1,493 @@
defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
use Pleroma.DataCase
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.OStatus
alias Pleroma.Activity
alias Pleroma.User
alias Pleroma.Repo
alias Pleroma.Web.Websub.WebsubClientSubscription
alias Pleroma.Web.Websub.WebsubServerSubscription
import Ecto.Query
import Pleroma.Factory
alias Pleroma.Web.CommonAPI
describe "handle_incoming" do
test "it ignores an incoming notice if we already have it" do
activity = insert(:note_activity)
data =
File.read!("test/fixtures/mastodon-post-activity.json")
|> Poison.decode!()
|> Map.put("object", activity.data["object"])
{:ok, returned_activity} = Transmogrifier.handle_incoming(data)
assert activity == returned_activity
end
test "it fetches replied-to activities if we don't have them" do
data =
File.read!("test/fixtures/mastodon-post-activity.json")
|> Poison.decode!()
object =
data["object"]
|> Map.put("inReplyTo", "https://shitposter.club/notice/2827873")
data =
data
|> Map.put("object", object)
{:ok, returned_activity} = Transmogrifier.handle_incoming(data)
assert activity =
Activity.get_create_activity_by_object_ap_id(
"tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment"
)
assert returned_activity.data["object"]["inReplyToAtomUri"] ==
"https://shitposter.club/notice/2827873"
assert returned_activity.data["object"]["inReplyToStatusId"] == activity.id
end
test "it works for incoming notices" do
data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!()
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["id"] ==
"http://mastodon.example.org/users/admin/statuses/99512778738411822/activity"
assert data["context"] ==
"tag:mastodon.example.org,2018-02-12:objectId=20:objectType=Conversation"
assert data["to"] == ["https://www.w3.org/ns/activitystreams#Public"]
assert data["cc"] == [
"http://mastodon.example.org/users/admin/followers",
"http://localtesting.pleroma.lol/users/lain"
]
assert data["actor"] == "http://mastodon.example.org/users/admin"
object = data["object"]
assert object["id"] == "http://mastodon.example.org/users/admin/statuses/99512778738411822"
assert object["to"] == ["https://www.w3.org/ns/activitystreams#Public"]
assert object["cc"] == [
"http://mastodon.example.org/users/admin/followers",
"http://localtesting.pleroma.lol/users/lain"
]
assert object["actor"] == "http://mastodon.example.org/users/admin"
assert object["attributedTo"] == "http://mastodon.example.org/users/admin"
assert object["context"] ==
"tag:mastodon.example.org,2018-02-12:objectId=20:objectType=Conversation"
assert object["sensitive"] == true
user = User.get_by_ap_id(object["actor"])
assert user.info["note_count"] == 1
end
test "it works for incoming notices with hashtags" do
data = File.read!("test/fixtures/mastodon-post-activity-hashtag.json") |> Poison.decode!()
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert Enum.at(data["object"]["tag"], 2) == "moo"
end
test "it works for incoming follow requests" do
user = insert(:user)
data =
File.read!("test/fixtures/mastodon-follow-activity.json")
|> Poison.decode!()
|> Map.put("object", user.ap_id)
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["actor"] == "http://mastodon.example.org/users/admin"
assert data["type"] == "Follow"
assert data["id"] == "http://mastodon.example.org/users/admin#follows/2"
assert User.following?(User.get_by_ap_id(data["actor"]), user)
end
test "it works for incoming likes" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
data =
File.read!("test/fixtures/mastodon-like.json")
|> Poison.decode!()
|> Map.put("object", activity.data["object"]["id"])
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["actor"] == "http://mastodon.example.org/users/admin"
assert data["type"] == "Like"
assert data["id"] == "http://mastodon.example.org/users/admin#likes/2"
assert data["object"] == activity.data["object"]["id"]
end
test "it works for incoming announces" do
data = File.read!("test/fixtures/mastodon-announce.json") |> Poison.decode!()
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["actor"] == "http://mastodon.example.org/users/admin"
assert data["type"] == "Announce"
assert data["id"] ==
"http://mastodon.example.org/users/admin/statuses/99542391527669785/activity"
assert data["object"] ==
"http://mastodon.example.org/users/admin/statuses/99541947525187367"
assert Activity.get_create_activity_by_object_ap_id(data["object"])
end
test "it works for incoming announces with an existing activity" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hey"})
data =
File.read!("test/fixtures/mastodon-announce.json")
|> Poison.decode!()
|> Map.put("object", activity.data["object"]["id"])
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["actor"] == "http://mastodon.example.org/users/admin"
assert data["type"] == "Announce"
assert data["id"] ==
"http://mastodon.example.org/users/admin/statuses/99542391527669785/activity"
assert data["object"] == activity.data["object"]["id"]
assert Activity.get_create_activity_by_object_ap_id(data["object"]).id == activity.id
end
test "it works for incoming update activities" do
data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!()
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
update_data = File.read!("test/fixtures/mastodon-update.json") |> Poison.decode!()
object =
update_data["object"]
|> Map.put("actor", data["actor"])
|> Map.put("id", data["actor"])
update_data =
update_data
|> Map.put("actor", data["actor"])
|> Map.put("object", object)
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(update_data)
user = User.get_cached_by_ap_id(data["actor"])
assert user.name == "gargle"
assert user.avatar["url"] == [
%{
"href" =>
"https://cd.niu.moe/accounts/avatars/000/033/323/original/fd7f8ae0b3ffedc9.jpeg"
}
]
assert user.info["banner"]["url"] == [
%{
"href" =>
"https://cd.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png"
}
]
assert user.bio == "<p>Some bio</p>"
end
test "it works for incoming deletes" do
activity = insert(:note_activity)
data =
File.read!("test/fixtures/mastodon-delete.json")
|> Poison.decode!()
object =
data["object"]
|> Map.put("id", activity.data["object"]["id"])
data =
data
|> Map.put("object", object)
|> Map.put("actor", activity.data["actor"])
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
refute Repo.get(Activity, activity.id)
end
test "it works for incoming unannounces with an existing notice" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hey"})
announce_data =
File.read!("test/fixtures/mastodon-announce.json")
|> Poison.decode!()
|> Map.put("object", activity.data["object"]["id"])
{:ok, %Activity{data: announce_data, local: false}} =
Transmogrifier.handle_incoming(announce_data)
data =
File.read!("test/fixtures/mastodon-undo-announce.json")
|> Poison.decode!()
|> Map.put("object", announce_data)
|> Map.put("actor", announce_data["actor"])
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["type"] == "Undo"
assert data["object"]["type"] == "Announce"
assert data["object"]["object"] == activity.data["object"]["id"]
assert data["object"]["id"] ==
"http://mastodon.example.org/users/admin/statuses/99542391527669785/activity"
end
test "it works for incomming unfollows" do
user = insert(:user)
follow_data =
File.read!("test/fixtures/mastodon-follow-activity.json")
|> Poison.decode!()
|> Map.put("object", user.ap_id)
{:ok, %Activity{data: _, local: false}} = Transmogrifier.handle_incoming(follow_data)
data =
File.read!("test/fixtures/mastodon-unfollow-activity.json")
|> Poison.decode!()
|> Map.put("object", follow_data)
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["type"] == "Undo"
assert data["object"]["type"] == "Follow"
+ assert data["object"]["object"] == user.ap_id
assert data["actor"] == "http://mastodon.example.org/users/admin"
refute User.following?(User.get_by_ap_id(data["actor"]), user)
end
+
+ test "it works for incoming blocks" do
+ user = insert(:user)
+
+ data =
+ File.read!("test/fixtures/mastodon-block-activity.json")
+ |> Poison.decode!()
+ |> Map.put("object", user.ap_id)
+
+ {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
+
+ assert data["type"] == "Block"
+ assert data["object"] == user.ap_id
+ assert data["actor"] == "https://mastodon.example.org/users/admin"
+
+ blocker = User.get_by_ap_id(data["actor"])
+
+ assert User.blocks?(blocker, user.ap_id)
+ end
+
+ test "it works for incoming unblocks" do
+ user = insert(:user)
+
+ block_data =
+ File.read!("test/fixtures/mastodon-block-activity.json")
+ |> Poison.decode!()
+ |> Map.put("object", user.ap_id)
+
+ {:ok, %Activity{data: _, local: false}} = Transmogrifier.handle_incoming(block_data)
+
+ data =
+ File.read!("test/fixtures/mastodon-unblock-activity.json")
+ |> Poison.decode!()
+ |> Map.put("object", block_data)
+
+ {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
+ assert data["type"] == "Undo"
+ assert data["object"]["type"] == "Block"
+ assert data["object"]["object"] == user.ap_id
+ assert data["actor"] == "https://mastodon.example.org/users/admin"
+
+ blocker = User.get_by_ap_id(data["actor"])
+
+ refute User.blocks?(blocker, user.ap_id)
+ end
end
describe "prepare outgoing" do
test "it turns mentions into tags" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{"status" => "hey, @#{other_user.nickname}, how are ya? #2hu"})
{:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
object = modified["object"]
expected_mention = %{
"href" => other_user.ap_id,
"name" => "@#{other_user.nickname}",
"type" => "Mention"
}
expected_tag = %{
"href" => Pleroma.Web.Endpoint.url() <> "/tags/2hu",
"type" => "Hashtag",
"name" => "#2hu"
}
assert Enum.member?(object["tag"], expected_tag)
assert Enum.member?(object["tag"], expected_mention)
end
test "it adds the sensitive property" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "#nsfw hey"})
{:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
assert modified["object"]["sensitive"]
end
test "it adds the json-ld context and the conversation property" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hey"})
{:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
assert modified["@context"] == "https://www.w3.org/ns/activitystreams"
assert modified["object"]["conversation"] == modified["context"]
end
test "it sets the 'attributedTo' property to the actor of the object if it doesn't have one" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hey"})
{:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
assert modified["object"]["actor"] == modified["object"]["attributedTo"]
end
test "it translates ostatus IDs to external URLs" do
incoming = File.read!("test/fixtures/incoming_note_activity.xml")
{:ok, [referent_activity]} = OStatus.handle_incoming(incoming)
user = insert(:user)
{:ok, activity, _} = CommonAPI.favorite(referent_activity.id, user)
{:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
assert modified["object"] == "http://gs.example.org:4040/index.php/notice/29"
end
test "it translates ostatus reply_to IDs to external URLs" do
incoming = File.read!("test/fixtures/incoming_note_activity.xml")
{:ok, [referred_activity]} = OStatus.handle_incoming(incoming)
user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{"status" => "HI!", "in_reply_to_status_id" => referred_activity.id})
{:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
assert modified["object"]["inReplyTo"] == "http://gs.example.org:4040/index.php/notice/29"
end
end
describe "user upgrade" do
test "it upgrades a user to activitypub" do
user =
insert(:user, %{
nickname: "rye@niu.moe",
local: false,
ap_id: "https://niu.moe/users/rye",
follower_address: User.ap_followers(%User{nickname: "rye@niu.moe"})
})
user_two = insert(:user, %{following: [user.follower_address]})
{:ok, activity} = CommonAPI.post(user, %{"status" => "test"})
{:ok, unrelated_activity} = CommonAPI.post(user_two, %{"status" => "test"})
assert "http://localhost:4001/users/rye@niu.moe/followers" in activity.recipients
user = Repo.get(User, user.id)
assert user.info["note_count"] == 1
{:ok, user} = Transmogrifier.upgrade_user_from_ap_id("https://niu.moe/users/rye")
assert user.info["ap_enabled"]
assert user.info["note_count"] == 1
assert user.follower_address == "https://niu.moe/users/rye/followers"
# Wait for the background task
:timer.sleep(1000)
user = Repo.get(User, user.id)
assert user.info["note_count"] == 1
activity = Repo.get(Activity, activity.id)
assert user.follower_address in activity.recipients
assert %{
"url" => [
%{
"href" =>
"https://cdn.niu.moe/accounts/avatars/000/033/323/original/fd7f8ae0b3ffedc9.jpeg"
}
]
} = user.avatar
assert %{
"url" => [
%{
"href" =>
"https://cdn.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png"
}
]
} = user.info["banner"]
refute "..." in activity.recipients
unrelated_activity = Repo.get(Activity, unrelated_activity.id)
refute user.follower_address in unrelated_activity.recipients
user_two = Repo.get(User, user_two.id)
assert user.follower_address in user_two.following
refute "..." in user_two.following
end
end
describe "maybe_retire_websub" do
test "it deletes all websub client subscripitions with the user as topic" do
subscription = %WebsubClientSubscription{topic: "https://niu.moe/users/rye.atom"}
{:ok, ws} = Repo.insert(subscription)
subscription = %WebsubClientSubscription{topic: "https://niu.moe/users/pasty.atom"}
{:ok, ws2} = Repo.insert(subscription)
Transmogrifier.maybe_retire_websub("https://niu.moe/users/rye")
refute Repo.get(WebsubClientSubscription, ws.id)
assert Repo.get(WebsubClientSubscription, ws2.id)
end
end
end
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Fri, Oct 9, 5:06 PM (1 d, 18 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1784798
Default Alt Text
(61 KB)
Attached To
Mode
rPUBE pleroma-upstream
Attached
Detach File
Event Timeline
Log In to Comment