diff --git a/config/config.exs b/config/config.exs
index 703c10c48..bd714a189 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -27,7 +27,8 @@
metadata: [:request_id]
config :mime, :types, %{
- "application/xrd+xml" => ["xrd+xml"]
+ "application/xrd+xml" => ["xrd+xml"],
+ "application/activity+json" => ["activity+json"]
}
config :pleroma, :websub, Pleroma.Web.Websub
diff --git a/config/dev.exs b/config/dev.exs
index 83326e6f2..a697d3a24 100644
--- a/config/dev.exs
+++ b/config/dev.exs
@@ -7,7 +7,7 @@
# watchers to your application. For example, we use it
# with brunch.io to recompile .js and .css sources.
config :pleroma, Pleroma.Web.Endpoint,
- http: [port: 4000],
+ http: [port: 4000, protocol_options: [max_request_line_length: 8192, max_header_value_length: 8192]],
protocol: "http",
debug_errors: true,
code_reloader: true,
diff --git a/lib/mix/tasks/fix_ap_users.ex b/lib/mix/tasks/fix_ap_users.ex
new file mode 100644
index 000000000..ff09074c3
--- /dev/null
+++ b/lib/mix/tasks/fix_ap_users.ex
@@ -0,0 +1,25 @@
+defmodule Mix.Tasks.FixApUsers do
+ use Mix.Task
+ import Mix.Ecto
+ import Ecto.Query
+ alias Pleroma.{Repo, User}
+
+ @shortdoc "Grab all ap users again"
+ def run([]) do
+ Mix.Task.run("app.start")
+
+ q = from u in User,
+ where: fragment("? @> ?", u.info, ^%{"ap_enabled" => true}),
+ where: u.local == false
+ users = Repo.all(q)
+
+ Enum.each(users, fn(user) ->
+ try do
+ IO.puts("Fetching #{user.nickname}")
+ Pleroma.Web.ActivityPub.Transmogrifier.upgrade_user_from_ap_id(user.ap_id, false)
+ rescue
+ e -> IO.inspect(e)
+ end
+ end)
+ end
+end
diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex
index afd09982f..a8154859a 100644
--- a/lib/pleroma/activity.ex
+++ b/lib/pleroma/activity.ex
@@ -7,6 +7,7 @@ defmodule Pleroma.Activity do
field :data, :map
field :local, :boolean, default: true
field :actor, :string
+ field :recipients, {:array, :string}
has_many :notifications, Notification, on_delete: :delete_all
timestamps()
diff --git a/lib/pleroma/plugs/http_signature.ex b/lib/pleroma/plugs/http_signature.ex
new file mode 100644
index 000000000..d2d4bdd63
--- /dev/null
+++ b/lib/pleroma/plugs/http_signature.ex
@@ -0,0 +1,27 @@
+defmodule Pleroma.Web.Plugs.HTTPSignaturePlug do
+ alias Pleroma.Web.HTTPSignatures
+ import Plug.Conn
+ require Logger
+
+ def init(options) do
+ options
+ end
+
+ def call(%{assigns: %{valid_signature: true}} = conn, opts) do
+ conn
+ end
+
+ def call(conn, opts) do
+ user = conn.params["actor"]
+ Logger.debug("Checking sig for #{user}")
+ if get_req_header(conn, "signature") do
+ conn = conn
+ |> put_req_header("(request-target)", String.downcase("#{conn.method}") <> " #{conn.request_path}")
+
+ assign(conn, :valid_signature, HTTPSignatures.validate_conn(conn))
+ else
+ Logger.debug("No signature header!")
+ conn
+ end
+ end
+end
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index 59f6340b8..a503a5b3f 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -80,9 +80,15 @@ def remote_user_creation(params) do
|> validate_length(:name, max: 100)
|> put_change(:local, false)
if changes.valid? do
- followers = User.ap_followers(%User{nickname: changes.changes[:nickname]})
- changes
- |> put_change(:follower_address, followers)
+ 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
@@ -97,6 +103,15 @@ def update_changeset(struct, params \\ %{}) do
|> 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])
@@ -144,11 +159,12 @@ def register_changeset(struct, params \\ %{}) do
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 do
+ if !followed.local && follower.local && !ap_enabled?(followed) do
Websub.subscribe(follower, followed)
end
@@ -202,6 +218,11 @@ def update_and_set_cache(changeset) do
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)
@@ -221,22 +242,30 @@ def get_cached_user_info(user) do
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} <- OStatus.make_user(nickname) do
+ {:ok, user} <- fetch_by_nickname(nickname) do
user
else _e -> nil
end
end
end
- # TODO: these queries could be more efficient if the type in postgresql wasn't map, but array.
def get_followers(%User{id: id, follower_address: follower_address}) do
q = from u in User,
- where: fragment("? @> ?", u.following, ^follower_address ),
+ where: ^follower_address in u.following,
where: u.id != ^id
{:ok, Repo.all(q)}
@@ -275,7 +304,7 @@ def update_note_count(%User{} = user) do
def update_follower_count(%User{} = user) do
follower_count_query = from u in User,
- where: fragment("? @> ?", u.following, ^user.follower_address),
+ where: ^user.follower_address in u.following,
where: u.id != ^user.id,
select: count(u.id)
@@ -288,7 +317,7 @@ def update_follower_count(%User{} = user) do
update_and_set_cache(cs)
end
- def get_notified_from_activity(%Activity{data: %{"to" => to}}) do
+ def get_notified_from_activity(%Activity{recipients: to}) do
query = from u in User,
where: u.ap_id in ^to,
where: u.local == true
@@ -296,10 +325,10 @@ def get_notified_from_activity(%Activity{data: %{"to" => to}}) do
Repo.all(query)
end
- def get_recipients_from_activity(%Activity{data: %{"to" => to}}) do
+ 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)
+ or_where: fragment("? && ?", u.following, ^to)
query = from u in query,
where: u.local == true
@@ -376,4 +405,57 @@ def delete (%User{} = user) do
: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
end
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index 421fd5cd7..965f2cc9b 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -1,14 +1,24 @@
defmodule Pleroma.Web.ActivityPub.ActivityPub do
alias Pleroma.{Activity, Repo, Object, Upload, User, Notification}
+ alias Pleroma.Web.ActivityPub.Transmogrifier
+ 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)
+
+ def get_recipients(data) do
+ (data["to"] || []) ++ (data["cc"] || [])
+ 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 <- insert_full_object(map) do
- {:ok, activity} = Repo.insert(%Activity{data: map, local: local, actor: map["actor"]})
+ {: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}
@@ -30,7 +40,11 @@ def stream_out(activity) do
end
end
- def create(to, actor, context, object, additional \\ %{}, published \\ nil, local \\ true) do
+ def create(%{to: to, actor: actor, context: context, object: object} = params) do
+ additional = params[:additional] || %{}
+ local = !(params[:local] == false) # only accept false as false value
+ 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) do
@@ -38,6 +52,26 @@ def create(to, actor, context, object, additional \\ %{}, published \\ nil, loca
end
end
+ def accept(%{to: to, actor: actor, object: object} = params) do
+ local = !(params[:local] == false) # only accept false as false value
+
+ 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 update(%{to: to, cc: cc, actor: actor, object: object} = params) do
+ local = !(params[:local] == false) # only accept false as false value
+
+ 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),
@@ -62,7 +96,8 @@ def unlike(%User{} = actor, %Object{} = object) do
end
def announce(%User{ap_id: _} = user, %Object{data: %{"id" => _}} = object, activity_id \\ nil, local \\ true) do
- with announce_data <- make_announce_data(user, object, activity_id),
+ 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
@@ -106,16 +141,26 @@ def delete(%Object{data: %{"id" => id, "actor" => actor}} = object, local \\ tru
end
def fetch_activities_for_context(context, opts \\ %{}) do
- query = from activity in Activity,
+ 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]
- query = restrict_blocked(query, opts)
Repo.all(query)
end
+ # TODO: Make this work properly with unlisted.
def fetch_public_activities(opts \\ %{}) do
- public = ["https://www.w3.org/ns/activitystreams#Public"]
- fetch_activities(public, opts)
+ q = fetch_activities_query(["https://www.w3.org/ns/activitystreams#Public"], opts)
+ q
+ |> Repo.all
+ |> Enum.reverse
end
defp restrict_since(query, %{"since_id" => since_id}) do
@@ -129,12 +174,15 @@ defp restrict_tag(query, %{"tag" => tag}) do
end
defp restrict_tag(query, _), do: query
- defp restrict_recipients(query, recipients) do
- Enum.reduce(recipients, query, fn (recipient, q) ->
- map = %{ to: [recipient] }
- from activity in q,
- or_where: fragment(~s(? @> ?), activity.data, ^map)
- end)
+ 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_local(query, %{"local_only" => true}) do
@@ -190,13 +238,13 @@ defp restrict_blocked(query, %{"blocking_user" => %User{info: info}}) do
end
defp restrict_blocked(query, _), do: query
- def fetch_activities(recipients, opts \\ %{}) do
+ 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)
+ |> restrict_recipients(recipients, opts["user"])
|> restrict_tag(opts)
|> restrict_since(opts)
|> restrict_local(opts)
@@ -207,6 +255,10 @@ def fetch_activities(recipients, opts \\ %{}) do
|> restrict_recent(opts)
|> restrict_blocked(opts)
|> restrict_media(opts)
+ end
+
+ def fetch_activities(recipients, opts \\ %{}) do
+ fetch_activities_query(recipients, opts)
|> Repo.all
|> Enum.reverse
end
@@ -215,4 +267,128 @@ 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"]}]
+ }
+
+ user_data = %{
+ ap_id: data["id"],
+ info: %{
+ "ap_enabled" => true,
+ "source_data" => data,
+ "banner" => banner
+ },
+ 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} <- Poison.decode(body) do
+ user_data_from_user_object(data)
+ else
+ e -> Logger.error("Could not 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
+
+ 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
+
+ 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
+
+ {:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
+ json = Poison.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}])
+ 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 {: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} <- Poison.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/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex
new file mode 100644
index 000000000..edbcb938a
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex
@@ -0,0 +1,54 @@
+defmodule Pleroma.Web.ActivityPub.ActivityPubController do
+ use Pleroma.Web, :controller
+ alias Pleroma.{User, Repo, Object, Activity}
+ alias Pleroma.Web.ActivityPub.{ObjectView, UserView, Transmogrifier}
+ alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.Federator
+
+ require Logger
+
+ action_fallback :errors
+
+ def user(conn, %{"nickname" => nickname}) do
+ with %User{} = user <- User.get_cached_by_nickname(nickname),
+ {:ok, user} <- Pleroma.Web.WebFinger.ensure_keys_present(user) do
+ conn
+ |> put_resp_header("content-type", "application/activity+json")
+ |> json(UserView.render("user.json", %{user: user}))
+ end
+ end
+
+ def object(conn, %{"uuid" => uuid}) do
+ with ap_id <- o_status_url(conn, :object, uuid),
+ %Object{} = object <- Object.get_cached_by_ap_id(ap_id) do
+ conn
+ |> put_resp_header("content-type", "application/activity+json")
+ |> json(ObjectView.render("object.json", %{object: object}))
+ end
+ end
+
+ # TODO: Ensure that this inbox is a recipient of the message
+ def inbox(%{assigns: %{valid_signature: true}} = conn, params) do
+ Federator.enqueue(:incoming_ap_doc, params)
+ json(conn, "ok")
+ end
+
+ def inbox(conn, params) do
+ headers = Enum.into(conn.req_headers, %{})
+ if !(String.contains?(headers["signature"] || "", params["actor"])) do
+ Logger.info("Signature not from author, relayed message, ignoring.")
+ else
+ Logger.info("Signature error.")
+ Logger.info("Could not validate #{params["actor"]}")
+ Logger.info(inspect(conn.req_headers))
+ end
+
+ json(conn, "ok")
+ end
+
+ def errors(conn, _e) do
+ conn
+ |> put_status(500)
+ |> json("error")
+ end
+end
diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex
new file mode 100644
index 000000000..37db67798
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/transmogrifier.ex
@@ -0,0 +1,298 @@
+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
+ 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
+
+ # 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
+
+ # TODO
+ # Accept
+ # Undo
+
+ 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 prepare_object(object) do
+ object
+ |> set_sensitive
+ |> add_hashtags
+ |> add_mention_tags
+ |> add_attributed_to
+ |> prepare_attachments
+ |> set_conversation
+ 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
+ |> Map.put("@context", "https://www.w3.org/ns/activitystreams")
+
+ {:ok, data}
+ 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
+
+ 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.
+ since = (Repo.aggregate(Activity, :max, :id) || 0) - 100_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 ac20a2822..cda106283 100644
--- a/lib/pleroma/web/activity_pub/utils.ex
+++ b/lib/pleroma/web/activity_pub/utils.ex
@@ -68,7 +68,7 @@ def lazy_put_object_defaults(map) do
@doc """
Inserts a full object if it is contained in an activity.
"""
- def insert_full_object(%{"object" => object_data}) when is_map(object_data) do
+ 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
@@ -109,6 +109,7 @@ def make_like_data(%User{ap_id: ap_id} = actor, %{data: %{"id" => id}} = object,
"actor" => ap_id,
"object" => id,
"to" => [actor.follower_address, object.data["actor"]],
+ "cc" => ["https://www.w3.org/ns/activitystreams#Public"],
"context" => object.data["context"]
}
@@ -150,6 +151,7 @@ def make_follow_data(%User{ap_id: follower_id}, %User{ap_id: followed_id}, activ
"type" => "Follow",
"actor" => follower_id,
"to" => [followed_id],
+ "cc" => ["https://www.w3.org/ns/activitystreams#Public"],
"object" => followed_id
}
@@ -177,6 +179,7 @@ def make_announce_data(%User{ap_id: ap_id} = user, %Object{data: %{"id" => id}}
"actor" => ap_id,
"object" => id,
"to" => [user.follower_address, object.data["actor"]],
+ "cc" => ["https://www.w3.org/ns/activitystreams#Public"],
"context" => object.data["context"]
}
@@ -205,7 +208,6 @@ def make_unfollow_data(follower, followed, follow_activity) do
def make_create_data(params, additional) do
published = params.published || make_date()
-
%{
"type" => "Create",
"to" => params.to |> Enum.uniq,
diff --git a/lib/pleroma/web/activity_pub/views/object_view.ex b/lib/pleroma/web/activity_pub/views/object_view.ex
new file mode 100644
index 000000000..cc0b0556b
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/views/object_view.ex
@@ -0,0 +1,27 @@
+defmodule Pleroma.Web.ActivityPub.ObjectView do
+ use Pleroma.Web, :view
+ alias Pleroma.Web.ActivityPub.Transmogrifier
+
+ def render("object.json", %{object: object}) do
+ base = %{
+ "@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"
+ }
+ ]
+ }
+
+ additional = Transmogrifier.prepare_object(object.data)
+ Map.merge(base, additional)
+ end
+end
diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex
new file mode 100644
index 000000000..179636884
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/views/user_view.ex
@@ -0,0 +1,57 @@
+defmodule Pleroma.Web.ActivityPub.UserView do
+ use Pleroma.Web, :view
+ alias Pleroma.Web.Salmon
+ alias Pleroma.Web.WebFinger
+ alias Pleroma.User
+
+ def render("user.json", %{user: user}) do
+ {:ok, user} = WebFinger.ensure_keys_present(user)
+ {:ok, _, public_key} = Salmon.keys_from_pem(user.info["keys"])
+ public_key = :public_key.pem_entry_encode(:RSAPublicKey, public_key)
+ public_key = :public_key.pem_encode([public_key])
+ %{
+ "@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"
+ }
+ ],
+ "id" => user.ap_id,
+ "type" => "Person",
+ "following" => "#{user.ap_id}/following",
+ "followers" => "#{user.ap_id}/followers",
+ "inbox" => "#{user.ap_id}/inbox",
+ "outbox" => "#{user.ap_id}/outbox",
+ "preferredUsername" => user.nickname,
+ "name" => user.name,
+ "summary" => user.bio,
+ "url" => user.ap_id,
+ "manuallyApprovesFollowers" => false,
+ "publicKey" => %{
+ "id" => "#{user.ap_id}#main-key",
+ "owner" => user.ap_id,
+ "publicKeyPem" => public_key
+ },
+ "endpoints" => %{
+ "sharedInbox" => "#{Pleroma.Web.Endpoint.url}/inbox"
+ },
+ "icon" => %{
+ "type" => "Image",
+ "url" => User.avatar_url(user)
+ },
+ "image" => %{
+ "type" => "Image",
+ "url" => User.banner_url(user)
+ }
+ }
+ end
+end
diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex
index 849360a16..0f84542f0 100644
--- a/lib/pleroma/web/common_api/common_api.ex
+++ b/lib/pleroma/web/common_api/common_api.ex
@@ -46,24 +46,36 @@ def unfavorite(id_or_ap_id, user) do
end
end
+ def get_visibility(%{"visibility" => visibility}), do: visibility
+ def get_visibility(%{"in_reply_to_status_id" => status_id}) when status_id do
+ inReplyTo = get_replied_to_activity(status_id)
+ Pleroma.Web.MastodonAPI.StatusView.get_visibility(inReplyTo.data["object"])
+ end
+ def get_visibility(_), do: "public"
+
@instance Application.get_env(:pleroma, :instance)
@limit Keyword.get(@instance, :limit)
def post(user, %{"status" => status} = data) do
+ visibility = get_visibility(data)
with status <- String.trim(status),
length when length in 1..@limit <- String.length(status),
attachments <- attachments_from_ids(data["media_ids"]),
mentions <- Formatter.parse_mentions(status),
inReplyTo <- get_replied_to_activity(data["in_reply_to_status_id"]),
- to <- to_for_user_and_mentions(user, mentions, inReplyTo),
+ {to, cc} <- to_for_user_and_mentions(user, mentions, inReplyTo, visibility),
tags <- Formatter.parse_tags(status, data),
content_html <- make_content_html(status, mentions, attachments, tags, data["no_attachment_links"]),
context <- make_context(inReplyTo),
cw <- data["spoiler_text"],
- object <- make_note_data(user.ap_id, to, context, content_html, attachments, inReplyTo, tags, cw),
+ object <- make_note_data(user.ap_id, to, context, content_html, attachments, inReplyTo, tags, cw, cc),
object <- Map.put(object, "emoji", Formatter.get_emoji(status) |> Enum.reduce(%{}, fn({name, file}, acc) -> Map.put(acc, name, "#{Pleroma.Web.Endpoint.static_url}#{file}") end)) do
- res = ActivityPub.create(to, user, context, object)
+ res = ActivityPub.create(%{to: to, actor: user, context: context, object: object, additional: %{"cc" => cc}})
User.increase_note_count(user)
res
end
end
+
+ def update(user) do
+ ActivityPub.update(%{local: true, to: [user.follower_address], cc: [], actor: user.ap_id, object: Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})})
+ end
end
diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex
index 2b359dd72..75c63e5f4 100644
--- a/lib/pleroma/web/common_api/utils.ex
+++ b/lib/pleroma/web/common_api/utils.ex
@@ -24,17 +24,34 @@ def attachments_from_ids(ids) do
end)
end
- def to_for_user_and_mentions(user, mentions, inReplyTo) do
- default_to = [
- user.follower_address,
- "https://www.w3.org/ns/activitystreams#Public"
- ]
+ def to_for_user_and_mentions(user, mentions, inReplyTo, "public") do
+ to = ["https://www.w3.org/ns/activitystreams#Public"]
- to = default_to ++ Enum.map(mentions, fn ({_, %{ap_id: ap_id}}) -> ap_id end)
+ mentioned_users = Enum.map(mentions, fn ({_, %{ap_id: ap_id}}) -> ap_id end)
+ cc = [user.follower_address | mentioned_users]
if inReplyTo do
- Enum.uniq([inReplyTo.data["actor"] | to])
+ {to, Enum.uniq([inReplyTo.data["actor"] | cc])}
else
- to
+ {to, cc}
+ end
+ end
+
+ def to_for_user_and_mentions(user, mentions, inReplyTo, "unlisted") do
+ {to, cc} = to_for_user_and_mentions(user, mentions, inReplyTo, "public")
+ {cc, to}
+ end
+
+ def to_for_user_and_mentions(user, mentions, inReplyTo, "private") do
+ {to, cc} = to_for_user_and_mentions(user, mentions, inReplyTo, "direct")
+ {[user.follower_address | to], cc}
+ end
+
+ def to_for_user_and_mentions(user, mentions, inReplyTo, "direct") do
+ mentioned_users = Enum.map(mentions, fn ({_, %{ap_id: ap_id}}) -> ap_id end)
+ if inReplyTo do
+ {Enum.uniq([inReplyTo.data["actor"] | mentioned_users]), []}
+ else
+ {mentioned_users, []}
end
end
@@ -99,10 +116,11 @@ def add_user_links(text, mentions) do
end)
end
- def make_note_data(actor, to, context, content_html, attachments, inReplyTo, tags, cw \\ nil) do
+ def make_note_data(actor, to, context, content_html, attachments, inReplyTo, tags, cw \\ nil, cc \\ []) do
object = %{
"type" => "Note",
"to" => to,
+ "cc" => cc,
"content" => content_html,
"summary" => cw,
"context" => context,
diff --git a/lib/pleroma/web/federator/federator.ex b/lib/pleroma/web/federator/federator.ex
index 8e28976a6..daf836a40 100644
--- a/lib/pleroma/web/federator/federator.ex
+++ b/lib/pleroma/web/federator/federator.ex
@@ -1,7 +1,10 @@
defmodule Pleroma.Web.Federator do
use GenServer
alias Pleroma.User
+ alias Pleroma.Activity
alias Pleroma.Web.{WebFinger, Websub}
+ alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.ActivityPub.Transmogrifier
require Logger
@websub Application.get_env(:pleroma, :websub)
@@ -44,11 +47,16 @@ def handle(:publish, activity) do
Logger.debug(fn -> "Running publish for #{activity.data["id"]}" end)
with actor when not is_nil(actor) <- User.get_cached_by_ap_id(activity.data["actor"]) do
{:ok, actor} = WebFinger.ensure_keys_present(actor)
- Logger.debug(fn -> "Sending #{activity.data["id"]} out via salmon" end)
- Pleroma.Web.Salmon.publish(actor, activity)
+ if ActivityPub.is_public?(activity) do
+ Logger.info(fn -> "Sending #{activity.data["id"]} out via websub" end)
+ Websub.publish(Pleroma.Web.OStatus.feed_path(actor), actor, activity)
- Logger.debug(fn -> "Sending #{activity.data["id"]} out via websub" end)
- Websub.publish(Pleroma.Web.OStatus.feed_path(actor), actor, activity)
+ Logger.info(fn -> "Sending #{activity.data["id"]} out via salmon" end)
+ Pleroma.Web.Salmon.publish(actor, activity)
+ end
+
+ Logger.info(fn -> "Sending #{activity.data["id"]} out via AP" end)
+ Pleroma.Web.ActivityPub.ActivityPub.publish(actor, activity)
end
end
@@ -58,10 +66,29 @@ def handle(:verify_websub, websub) do
end
def handle(:incoming_doc, doc) do
- Logger.debug("Got document, trying to parse")
+ Logger.info("Got document, trying to parse")
@ostatus.handle_incoming(doc)
end
+ def handle(:incoming_ap_doc, params) do
+ Logger.info("Handling incoming ap activity")
+ with {:ok, _user} <- ap_enabled_actor(params["actor"]),
+ nil <- Activity.get_by_ap_id(params["id"]),
+ {:ok, activity} <- Transmogrifier.handle_incoming(params) do
+ else
+ %Activity{} ->
+ Logger.info("Already had #{params["id"]}")
+ e ->
+ # Just drop those for now
+ Logger.info("Unhandled activity")
+ Logger.info(Poison.encode!(params, [pretty: 2]))
+ end
+ end
+
+ def handle(:publish_single_ap, params) do
+ ActivityPub.publish_one(params)
+ end
+
def handle(:publish_single_websub, %{xml: xml, topic: topic, callback: callback, secret: secret}) do
signature = @websub.sign(secret || "", xml)
Logger.debug(fn -> "Pushing #{topic} to #{callback}" end)
@@ -102,7 +129,7 @@ def maybe_start_job(running_jobs, queue) do
end
end
- def handle_cast({:enqueue, type, payload, priority}, state) when type in [:incoming_doc] do
+ def handle_cast({:enqueue, type, payload, priority}, state) when type in [:incoming_doc, :incoming_ap_doc] do
%{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}} = state
i_queue = enqueue_sorted(i_queue, {type, payload}, 1)
{i_running_jobs, i_queue} = maybe_start_job(i_running_jobs, i_queue)
@@ -139,4 +166,13 @@ def enqueue_sorted(queue, element, priority) do
def queue_pop([%{item: element} | queue]) do
{element, queue}
end
+
+ def ap_enabled_actor(id) do
+ user = User.get_by_ap_id(id)
+ if User.ap_enabled?(user) do
+ {:ok, user}
+ else
+ ActivityPub.make_user_from_ap_id(id)
+ end
+ end
end
diff --git a/lib/pleroma/web/http_signatures/http_signatures.ex b/lib/pleroma/web/http_signatures/http_signatures.ex
new file mode 100644
index 000000000..93c7c310d
--- /dev/null
+++ b/lib/pleroma/web/http_signatures/http_signatures.ex
@@ -0,0 +1,79 @@
+# https://tools.ietf.org/html/draft-cavage-http-signatures-08
+defmodule Pleroma.Web.HTTPSignatures do
+ alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.ActivityPub
+ require Logger
+
+ def split_signature(sig) do
+ default = %{"headers" => "date"}
+
+ sig = sig
+ |> String.trim()
+ |> String.split(",")
+ |> Enum.reduce(default, fn(part, acc) ->
+ [key | rest] = String.split(part, "=")
+ value = Enum.join(rest, "=")
+ Map.put(acc, key, String.trim(value, "\""))
+ end)
+
+ Map.put(sig, "headers", String.split(sig["headers"], ~r/\s/))
+ end
+
+ def validate(headers, signature, public_key) do
+ sigstring = build_signing_string(headers, signature["headers"])
+ {:ok, sig} = Base.decode64(signature["signature"])
+ :public_key.verify(sigstring, :sha256, sig, public_key)
+ end
+
+ def validate_conn(conn) do
+ # TODO: How to get the right key and see if it is actually valid for that request.
+ # For now, fetch the key for the actor.
+ with actor_id <- conn.params["actor"],
+ {:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do
+ if validate_conn(conn, public_key) do
+ true
+ else
+ Logger.debug("Could not validate, re-fetching user and trying one more time.")
+ # Fetch user anew and try one more time
+ with actor_id <- conn.params["actor"],
+ {:ok, _user} <- ActivityPub.make_user_from_ap_id(actor_id),
+ {:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do
+ validate_conn(conn, public_key)
+ end
+ end
+ else
+ e ->
+ Logger.debug("Could not public key!")
+ end
+ end
+
+ def validate_conn(conn, public_key) do
+ headers = Enum.into(conn.req_headers, %{})
+ signature = split_signature(headers["signature"])
+ validate(headers, signature, public_key)
+ end
+
+ def build_signing_string(headers, used_headers) do
+ used_headers
+ |> Enum.map(fn (header) -> "#{header}: #{headers[header]}" end)
+ |> Enum.join("\n")
+ end
+
+ def sign(user, headers) do
+ with {:ok, %{info: %{"keys" => keys}}} <- Pleroma.Web.WebFinger.ensure_keys_present(user),
+ {:ok, private_key, _} = Pleroma.Web.Salmon.keys_from_pem(keys) do
+ sigstring = build_signing_string(headers, Map.keys(headers))
+ signature = :public_key.sign(sigstring, :sha256, private_key)
+ |> Base.encode64()
+
+ [
+ keyId: user.ap_id <> "#main-key",
+ algorithm: "rsa-sha256",
+ headers: Map.keys(headers) |> Enum.join(" "),
+ signature: signature
+ ]
+ |> Enum.map(fn({k, v}) -> "#{k}=\"#{v}\"" end)
+ |> Enum.join(",")
+ end
+ 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 e16a2a092..fbf8c1915 100644
--- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
+++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
@@ -24,6 +24,7 @@ def create_app(conn, params) do
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
@@ -40,7 +41,7 @@ def update_credentials(%{assigns: %{user: user}} = conn, params) do
with %Plug.Upload{} <- avatar,
{:ok, object} <- ActivityPub.upload(avatar),
change = Ecto.Changeset.change(user, %{avatar: object.data}),
- {:ok, user} = Repo.update(change) do
+ {:ok, user} = User.update_and_set_cache(change) do
user
else
_e -> user
@@ -54,7 +55,7 @@ def update_credentials(%{assigns: %{user: user}} = conn, params) do
{:ok, object} <- ActivityPub.upload(banner),
new_info <- Map.put(user.info, "banner", object.data),
change <- User.info_changeset(user, %{info: new_info}),
- {:ok, user} <- Repo.update(change) do
+ {:ok, user} <- User.update_and_set_cache(change) do
user
else
_e -> user
@@ -64,7 +65,10 @@ def update_credentials(%{assigns: %{user: user}} = conn, params) do
end
with changeset <- User.update_changeset(user, params),
- {:ok, user} <- Repo.update(changeset) do
+ {: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 ->
@@ -150,6 +154,7 @@ 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
@@ -181,7 +186,7 @@ def user_statuses(%{assigns: %{user: user}} = conn, params) do
|> Map.put("actor_id", ap_id)
|> Map.put("whole_db", true)
- activities = ActivityPub.fetch_activities([], params)
+ activities = ActivityPub.fetch_public_activities(params)
|> Enum.reverse
render conn, StatusView, "index.json", %{activities: activities, for: user, as: :activity}
@@ -189,14 +194,15 @@ def user_statuses(%{assigns: %{user: user}} = conn, params) do
end
def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
- with %Activity{} = activity <- Repo.get(Activity, 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["object"]["context"], %{"blocking_user" => user}),
+ 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
@@ -463,12 +469,12 @@ def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) d
end
def favourites(%{assigns: %{user: user}} = conn, _) do
- params = conn
+ params = %{}
|> Map.put("type", "Create")
|> Map.put("favorited_by", user.ap_id)
|> Map.put("blocking_user", user)
- activities = ActivityPub.fetch_activities([], params)
+ activities = ActivityPub.fetch_public_activities(params)
|> Enum.reverse
conn
diff --git a/lib/pleroma/web/mastodon_api/mastodon_socket.ex b/lib/pleroma/web/mastodon_api/mastodon_socket.ex
index fe71ea271..c3bae5935 100644
--- a/lib/pleroma/web/mastodon_api/mastodon_socket.ex
+++ b/lib/pleroma/web/mastodon_api/mastodon_socket.ex
@@ -25,7 +25,6 @@ def connect(params, socket) do
def id(_), do: nil
def handle(:text, message, _state) do
- IO.inspect message
#| :ok
#| state
#| {:text, message}
diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex
index d2a4dd366..f378bb36e 100644
--- a/lib/pleroma/web/mastodon_api/views/account_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/account_view.ex
@@ -18,7 +18,7 @@ def render("account.json", %{user: user}) do
id: to_string(user.id),
username: hd(String.split(user.nickname, "@")),
acct: user.nickname,
- display_name: user.name,
+ display_name: user.name || user.nickname,
locked: false,
created_at: Utils.to_masto_date(user.inserted_at),
followers_count: user_info.follower_count,
diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex
index 64f315597..4f395d0f7 100644
--- a/lib/pleroma/web/mastodon_api/views/status_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/status_view.ex
@@ -58,7 +58,7 @@ def render("status.json", %{activity: %{data: %{"object" => object}} = activity}
announcement_count = object["announcement_count"] || 0
tags = object["tag"] || []
- sensitive = Enum.member?(tags, "nsfw")
+ sensitive = object["sensitive"] || Enum.member?(tags, "nsfw")
mentions = activity.data["to"]
|> Enum.map(fn (ap_id) -> User.get_cached_by_ap_id(ap_id) end)
@@ -96,7 +96,7 @@ def render("status.json", %{activity: %{data: %{"object" => object}} = activity}
muted: false,
sensitive: sensitive,
spoiler_text: object["summary"] || "",
- visibility: "public",
+ visibility: get_visibility(object),
media_attachments: attachments |> Enum.take(4),
mentions: mentions,
tags: [], # fix,
@@ -109,6 +109,18 @@ def render("status.json", %{activity: %{data: %{"object" => object}} = activity}
}
end
+ def get_visibility(object) do
+ public = "https://www.w3.org/ns/activitystreams#Public"
+ to = object["to"] || []
+ cc = object["cc"] || []
+ cond do
+ public in to -> "public"
+ public in cc -> "unlisted"
+ Enum.any?(to, &(String.contains?(&1, "/followers"))) -> "private"
+ true -> "direct"
+ end
+ end
+
def render("attachment.json", %{attachment: attachment}) do
[%{"mediaType" => media_type, "href" => href} | _] = attachment["url"]
diff --git a/lib/pleroma/web/ostatus/activity_representer.ex b/lib/pleroma/web/ostatus/activity_representer.ex
index aa2b1df39..c8ade52a4 100644
--- a/lib/pleroma/web/ostatus/activity_representer.ex
+++ b/lib/pleroma/web/ostatus/activity_representer.ex
@@ -76,10 +76,17 @@ def to_simple_form(%{data: %{"object" => %{"type" => "Note"}}} = activity, user,
in_reply_to = get_in_reply_to(activity.data)
author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: []
- mentions = activity.data["to"] |> get_mentions
+ mentions = activity.recipients |> get_mentions
categories = (activity.data["object"]["tag"] || [])
- |> Enum.map(fn (tag) -> {:category, [term: to_charlist(tag)], []} end)
+ |> Enum.map(fn (tag) ->
+ if is_binary(tag) do
+ {:category, [term: to_charlist(tag)], []}
+ else
+ nil
+ end
+ end)
+ |> Enum.filter(&(&1))
emoji_links = get_emoji_links(activity.data["object"]["emoji"] || %{})
@@ -110,7 +117,7 @@ def to_simple_form(%{data: %{"type" => "Like"}} = activity, user, with_author) d
_in_reply_to = get_in_reply_to(activity.data)
author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: []
- mentions = activity.data["to"] |> get_mentions
+ mentions = activity.recipients |> get_mentions
[
{:"activity:verb", ['http://activitystrea.ms/schema/1.0/favorite']},
@@ -144,7 +151,7 @@ def to_simple_form(%{data: %{"type" => "Announce"}} = activity, user, with_autho
retweeted_xml = to_simple_form(retweeted_activity, retweeted_user, true)
- mentions = activity.data["to"] |> get_mentions
+ mentions = activity.recipients |> get_mentions
[
{:"activity:object-type", ['http://activitystrea.ms/schema/1.0/activity']},
{:"activity:verb", ['http://activitystrea.ms/schema/1.0/share']},
@@ -168,7 +175,7 @@ def to_simple_form(%{data: %{"type" => "Follow"}} = activity, user, with_author)
author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: []
- mentions = (activity.data["to"] || []) |> get_mentions
+ mentions = (activity.recipients || []) |> get_mentions
[
{:"activity:object-type", ['http://activitystrea.ms/schema/1.0/activity']},
{:"activity:verb", ['http://activitystrea.ms/schema/1.0/follow']},
@@ -196,7 +203,7 @@ def to_simple_form(%{data: %{"type" => "Undo"}} = activity, user, with_author) d
author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: []
follow_activity = Activity.get_by_ap_id(activity.data["object"])
- mentions = (activity.data["to"] || []) |> get_mentions
+ mentions = (activity.recipients || []) |> get_mentions
[
{:"activity:object-type", ['http://activitystrea.ms/schema/1.0/activity']},
{:"activity:verb", ['http://activitystrea.ms/schema/1.0/unfollow']},
diff --git a/lib/pleroma/web/ostatus/handlers/note_handler.ex b/lib/pleroma/web/ostatus/handlers/note_handler.ex
index 8747dbb67..38f9fc478 100644
--- a/lib/pleroma/web/ostatus/handlers/note_handler.ex
+++ b/lib/pleroma/web/ostatus/handlers/note_handler.ex
@@ -88,6 +88,7 @@ def fetch_replied_to_activity(entry, inReplyTo) do
end
end
+ # TODO: Clean this up a bit.
def handle_note(entry, doc \\ nil) do
with id <- XML.string_from_xpath("//id", entry),
activity when is_nil(activity) <- Activity.get_create_activity_by_object_ap_id(id),
@@ -104,15 +105,18 @@ def handle_note(entry, doc \\ nil) do
mentions <- get_mentions(entry),
to <- make_to_list(actor, mentions),
date <- XML.string_from_xpath("//published", entry),
+ unlisted <- XML.string_from_xpath("//mastodon:scope", entry) == "unlisted",
+ cc <- if(unlisted, do: ["https://www.w3.org/ns/activitystreams#Public"], else: []),
note <- CommonAPI.Utils.make_note_data(actor.ap_id, to, context, content_html, attachments, inReplyToActivity, [], cw),
note <- note |> Map.put("id", id) |> Map.put("tag", tags),
note <- note |> Map.put("published", date),
note <- note |> Map.put("emoji", get_emoji(entry)),
note <- add_external_url(note, entry),
+ note <- note |> Map.put("cc", cc),
# TODO: Handle this case in make_note_data
note <- (if inReplyTo && !inReplyToActivity, do: note |> Map.put("inReplyTo", inReplyTo), else: note)
do
- res = ActivityPub.create(to, actor, context, note, %{}, date, false)
+ res = ActivityPub.create(%{to: to, actor: actor, context: context, object: note, published: date, local: false, additional: %{"cc" => cc}})
User.increase_note_count(actor)
res
else
diff --git a/lib/pleroma/web/ostatus/ostatus.ex b/lib/pleroma/web/ostatus/ostatus.ex
index c35ba42be..bed15e8c0 100644
--- a/lib/pleroma/web/ostatus/ostatus.ex
+++ b/lib/pleroma/web/ostatus/ostatus.ex
@@ -9,6 +9,7 @@ defmodule Pleroma.Web.OStatus do
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.{WebFinger, Websub}
alias Pleroma.Web.OStatus.{FollowHandler, NoteHandler, DeleteHandler}
+ alias Pleroma.Web.ActivityPub.Transmogrifier
def feed_path(user) do
"#{user.ap_id}/feed.atom"
@@ -177,6 +178,13 @@ def get_tags(entry) do
end
def maybe_update(doc, user) do
+ if "true" == string_from_xpath("//author[1]/ap_enabled", doc) do
+ Transmogrifier.upgrade_user_from_ap_id(user.ap_id)
+ else
+ maybe_update_ostatus(doc, user)
+ end
+ end
+ def maybe_update_ostatus(doc, user) do
old_data = %{
avatar: user.avatar,
bio: user.bio,
@@ -218,11 +226,6 @@ def find_or_make_user(uri) do
end
end
- def insert_or_update_user(data) do
- cs = User.remote_user_creation(data)
- Repo.insert(cs, on_conflict: :replace_all, conflict_target: :nickname)
- end
-
def make_user(uri, update \\ false) do
with {:ok, info} <- gather_user_info(uri) do
data = %{
@@ -236,7 +239,7 @@ def make_user(uri, update \\ false) do
with false <- update,
%User{} = user <- User.get_by_ap_id(data.ap_id) do
{:ok, user}
- else _e -> insert_or_update_user(data)
+ else _e -> User.insert_or_update_user(data)
end
end
end
@@ -297,7 +300,10 @@ def fetch_activity_from_atom_url(url) do
with {:ok, %{body: body, status_code: code}} when code in 200..299 <- @httpoison.get(url, [Accept: "application/atom+xml"], follow_redirect: true, timeout: 10000, recv_timeout: 20000) do
Logger.debug("Got document from #{url}, handling...")
handle_incoming(body)
- else e -> Logger.debug("Couldn't get #{url}: #{inspect(e)}")
+ else
+ e ->
+ Logger.debug("Couldn't get #{url}: #{inspect(e)}")
+ e
end
end
@@ -306,7 +312,10 @@ def fetch_activity_from_html_url(url) do
with {:ok, %{body: body}} <- @httpoison.get(url, [], follow_redirect: true, timeout: 10000, recv_timeout: 20000),
{:ok, atom_url} <- get_atom_url(body) do
fetch_activity_from_atom_url(atom_url)
- else e -> Logger.debug("Couldn't get #{url}: #{inspect(e)}")
+ else
+ e ->
+ Logger.debug("Couldn't get #{url}: #{inspect(e)}")
+ e
end
end
diff --git a/lib/pleroma/web/ostatus/ostatus_controller.ex b/lib/pleroma/web/ostatus/ostatus_controller.ex
index 4d48c5d2b..cb435e031 100644
--- a/lib/pleroma/web/ostatus/ostatus_controller.ex
+++ b/lib/pleroma/web/ostatus/ostatus_controller.ex
@@ -6,27 +6,25 @@ defmodule Pleroma.Web.OStatus.OStatusController do
alias Pleroma.Repo
alias Pleroma.Web.{OStatus, Federator}
alias Pleroma.Web.XML
+ alias Pleroma.Web.ActivityPub.ActivityPubController
+ alias Pleroma.Web.ActivityPub.ActivityPub
import Ecto.Query
- def feed_redirect(conn, %{"nickname" => nickname}) do
+ def feed_redirect(conn, %{"nickname" => nickname} = params) do
user = User.get_cached_by_nickname(nickname)
case get_format(conn) do
"html" -> Fallback.RedirectController.redirector(conn, nil)
+ "activity+json" -> ActivityPubController.user(conn, params)
_ -> redirect conn, external: OStatus.feed_path(user)
end
end
def feed(conn, %{"nickname" => nickname} = params) do
user = User.get_cached_by_nickname(nickname)
- query = from activity in Activity,
- where: fragment("?->>'actor' = ?", activity.data, ^user.ap_id),
- limit: 20,
- order_by: [desc: :id]
- activities = query
- |> restrict_max(params)
- |> Repo.all
+ activities = ActivityPub.fetch_public_activities(%{"whole_db" => true, "actor_id" => user.ap_id})
+ |> Enum.reverse
response = user
|> FeedRepresenter.to_simple_form(activities, [user])
@@ -55,11 +53,6 @@ defp decode_or_retry(body) do
end
end
- 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
-
def salmon_incoming(conn, _) do
{:ok, body, _conn} = read_body(conn)
{:ok, doc} = decode_or_retry(body)
@@ -70,17 +63,23 @@ def salmon_incoming(conn, _) do
|> send_resp(200, "")
end
- def object(conn, %{"uuid" => uuid}) do
- with id <- o_status_url(conn, :object, uuid),
- %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id),
- %User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
- case get_format(conn) do
- "html" -> redirect(conn, to: "/notice/#{activity.id}")
- _ -> represent_activity(conn, activity, user)
+ # TODO: Data leak
+ def object(conn, %{"uuid" => uuid} = params) do
+ if get_format(conn) == "activity+json" do
+ ActivityPubController.object(conn, params)
+ else
+ with id <- o_status_url(conn, :object, uuid),
+ %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id),
+ %User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
+ case get_format(conn) do
+ "html" -> redirect(conn, to: "/notice/#{activity.id}")
+ _ -> represent_activity(conn, activity, user)
+ end
end
end
end
+ # TODO: Data leak
def activity(conn, %{"uuid" => uuid}) do
with id <- o_status_url(conn, :activity, uuid),
%Activity{} = activity <- Activity.get_by_ap_id(id),
@@ -92,6 +91,7 @@ def activity(conn, %{"uuid" => uuid}) do
end
end
+ # TODO: Data leak
def notice(conn, %{"id" => id}) do
with %Activity{} = activity <- Repo.get(Activity, id),
%User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
diff --git a/lib/pleroma/web/ostatus/user_representer.ex b/lib/pleroma/web/ostatus/user_representer.ex
index 20ebb3e08..5af439c9d 100644
--- a/lib/pleroma/web/ostatus/user_representer.ex
+++ b/lib/pleroma/web/ostatus/user_representer.ex
@@ -12,6 +12,12 @@ def to_simple_form(user) do
[]
end
+ ap_enabled = if user.local do
+ [{:ap_enabled, ['true']}]
+ else
+ []
+ end
+
[
{:id, [ap_id]},
{:"activity:object", ['http://activitystrea.ms/schema/1.0/person']},
@@ -22,6 +28,6 @@ def to_simple_form(user) do
{:summary, [bio]},
{:name, [nickname]},
{:link, [rel: 'avatar', href: avatar_url], []}
- ] ++ banner
+ ] ++ banner ++ ap_enabled
end
end
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 59fb5cd6b..520ac4a8c 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -222,7 +222,7 @@ def user_fetcher(username) do
end
pipeline :ostatus do
- plug :accepts, ["xml", "atom", "html"]
+ plug :accepts, ["xml", "atom", "html", "activity+json"]
end
scope "/", Pleroma.Web do
@@ -243,7 +243,18 @@ def user_fetcher(username) do
end
+ pipeline :activitypub do
+ plug :accepts, ["activity+json"]
+ plug Pleroma.Web.Plugs.HTTPSignaturePlug
+ 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
diff --git a/lib/pleroma/web/salmon/salmon.ex b/lib/pleroma/web/salmon/salmon.ex
index 81b864582..46ca645d1 100644
--- a/lib/pleroma/web/salmon/salmon.ex
+++ b/lib/pleroma/web/salmon/salmon.ex
@@ -29,7 +29,8 @@ def fetch_magic_key(salmon) do
with [data, _, _, _, _] <- decode(salmon),
doc <- XML.parse_document(data),
uri when not is_nil(uri) <- XML.string_from_xpath("/entry/author[1]/uri", doc),
- {:ok, %{info: %{"magic_key" => magic_key}}} <- Pleroma.Web.OStatus.find_or_make_user(uri) do
+ {:ok, public_key} <- User.get_public_key_for_ap_id(uri),
+ magic_key <- encode_key(public_key) do
{:ok, magic_key}
end
end
@@ -138,7 +139,8 @@ def encode(private_key, doc) do
{:ok, salmon}
end
- def remote_users(%{data: %{"to" => to}}) do
+ def remote_users(%{data: %{"to" => to} = data}) do
+ to = to ++ (data["cc"] || [])
to
|> Enum.map(fn(id) -> User.get_cached_by_ap_id(id) end)
|> Enum.filter(fn(user) -> user && !user.local end)
@@ -154,8 +156,16 @@ defp send_to_user(%{info: %{"salmon" => salmon}}, feed, poster) do
defp send_to_user(_,_,_), do: nil
+ @supported_activities [
+ "Create",
+ "Follow",
+ "Like",
+ "Announce",
+ "Undo",
+ "Delete"
+ ]
def publish(user, activity, poster \\ &@httpoison.post/4)
- def publish(%{info: %{"keys" => keys}} = user, activity, poster) do
+ def publish(%{info: %{"keys" => keys}} = user, %{data: %{"type" => type}} = activity, poster) when type in @supported_activities do
feed = ActivityRepresenter.to_simple_form(activity, user, true)
|> ActivityRepresenter.wrap_with_entry
|> :xmerl.export_simple(:xmerl_xml)
diff --git a/lib/pleroma/web/streamer.ex b/lib/pleroma/web/streamer.ex
index d64e6c393..a417178ba 100644
--- a/lib/pleroma/web/streamer.ex
+++ b/lib/pleroma/web/streamer.ex
@@ -74,7 +74,6 @@ def handle_cast(%{action: :add, topic: topic, socket: socket}, sockets) do
sockets_for_topic = Enum.uniq([socket | sockets_for_topic])
sockets = Map.put(sockets, topic, sockets_for_topic)
Logger.debug("Got new conn for #{topic}")
- IO.inspect(sockets)
{:noreply, sockets}
end
@@ -84,12 +83,11 @@ def handle_cast(%{action: :remove, topic: topic, socket: socket}, sockets) do
sockets_for_topic = List.delete(sockets_for_topic, socket)
sockets = Map.put(sockets, topic, sockets_for_topic)
Logger.debug("Removed conn for #{topic}")
- IO.inspect(sockets)
{:noreply, sockets}
end
def handle_cast(m, state) do
- IO.inspect("Unknown: #{inspect(m)}, #{inspect(state)}")
+ Logger.info("Unknown: #{inspect(m)}, #{inspect(state)}")
{:noreply, state}
end
diff --git a/lib/pleroma/web/twitter_api/representers/activity_representer.ex b/lib/pleroma/web/twitter_api/representers/activity_representer.ex
index 1f11bc9ac..5199cef8e 100644
--- a/lib/pleroma/web/twitter_api/representers/activity_representer.ex
+++ b/lib/pleroma/web/twitter_api/representers/activity_representer.ex
@@ -56,7 +56,8 @@ def to_map(%Activity{data: %{"type" => "Like", "published" => created_at}} = act
}
end
- def to_map(%Activity{data: %{"type" => "Follow", "published" => created_at, "object" => followed_id}} = activity, %{user: user} = opts) do
+ def to_map(%Activity{data: %{"type" => "Follow", "object" => followed_id}} = activity, %{user: user} = opts) do
+ created_at = activity.data["published"] || (DateTime.to_iso8601(activity.inserted_at))
created_at = created_at |> Utils.date_to_asctime
followed = User.get_cached_by_ap_id(followed_id)
@@ -125,7 +126,7 @@ def to_map(%Activity{data: %{"object" => %{"content" => content} = object}} = ac
mentions = opts[:mentioned] || []
- attentions = activity.data["to"]
+ attentions = activity.recipients
|> Enum.map(fn (ap_id) -> Enum.find(mentions, fn(user) -> ap_id == user.ap_id end) end)
|> Enum.filter(&(&1))
|> Enum.map(fn (user) -> UserView.render("show.json", %{user: user, for: opts[:for]}) end)
@@ -133,7 +134,9 @@ def to_map(%Activity{data: %{"object" => %{"content" => content} = object}} = ac
conversation_id = conversation_id(activity)
tags = activity.data["object"]["tag"] || []
- possibly_sensitive = Enum.member?(tags, "nsfw")
+ possibly_sensitive = activity.data["object"]["sensitive"] || Enum.member?(tags, "nsfw")
+
+ tags = if possibly_sensitive, do: Enum.uniq(["nsfw" | tags]), else: tags
summary = activity.data["object"]["summary"]
content = if !!summary and summary != "" do
@@ -161,7 +164,7 @@ def to_map(%Activity{data: %{"object" => %{"content" => content} = object}} = ac
"repeat_num" => announcement_count,
"favorited" => to_boolean(favorited),
"repeated" => to_boolean(repeated),
- "external_url" => object["external_url"],
+ "external_url" => object["external_url"] || object["id"],
"tags" => tags,
"activity_type" => "post",
"possibly_sensitive" => possibly_sensitive
diff --git a/lib/pleroma/web/twitter_api/representers/object_representer.ex b/lib/pleroma/web/twitter_api/representers/object_representer.ex
index 69eaeb36c..e2d653ba8 100644
--- a/lib/pleroma/web/twitter_api/representers/object_representer.ex
+++ b/lib/pleroma/web/twitter_api/representers/object_representer.ex
@@ -2,9 +2,8 @@ defmodule Pleroma.Web.TwitterAPI.Representers.ObjectRepresenter do
use Pleroma.Web.TwitterAPI.Representers.BaseRepresenter
alias Pleroma.Object
- def to_map(%Object{} = object, _opts) do
+ def to_map(%Object{data: %{"url" => [url | _]}} = object, _opts) do
data = object.data
- url = List.first(data["url"])
%{
url: url["href"] |> Pleroma.Web.MediaProxy.url(),
mimetype: url["mediaType"],
@@ -13,6 +12,19 @@ def to_map(%Object{} = object, _opts) do
}
end
+ def to_map(%Object{data: %{"url" => url} = data}, _opts) when is_binary(url) do
+ %{
+ url: url |> Pleroma.Web.MediaProxy.url(),
+ mimetype: data["mediaType"],
+ id: data["uuid"],
+ oembed: false
+ }
+ end
+
+ def to_map(%Object{}, _opts) do
+ %{}
+ end
+
# If we only get the naked data, wrap in an object
def to_map(%{} = data, opts) do
to_map(%Object{data: data}, opts)
diff --git a/lib/pleroma/web/twitter_api/twitter_api.ex b/lib/pleroma/web/twitter_api/twitter_api.ex
index faecebde0..987a960bb 100644
--- a/lib/pleroma/web/twitter_api/twitter_api.ex
+++ b/lib/pleroma/web/twitter_api/twitter_api.ex
@@ -13,26 +13,38 @@ def create_status(%User{} = user, %{"status" => _} = data) do
end
def fetch_friend_statuses(user, opts \\ %{}) do
- opts = Map.put(opts, "blocking_user", user)
+ opts = opts
+ |> Map.put("blocking_user", user)
+ |> Map.put("user", user)
+ |> Map.put("type", ["Create", "Announce", "Follow", "Like"])
+
ActivityPub.fetch_activities([user.ap_id | user.following], opts)
|> activities_to_statuses(%{for: user})
end
def fetch_public_statuses(user, opts \\ %{}) do
- opts = Map.put(opts, "local_only", true)
- opts = Map.put(opts, "blocking_user", user)
+ opts = opts
+ |> Map.put("local_only", true)
+ |> Map.put("blocking_user", user)
+ |> Map.put("type", ["Create", "Announce", "Follow"])
+
ActivityPub.fetch_public_activities(opts)
|> activities_to_statuses(%{for: user})
end
def fetch_public_and_external_statuses(user, opts \\ %{}) do
- opts = Map.put(opts, "blocking_user", user)
+ opts = opts
+ |> Map.put("blocking_user", user)
+ |> Map.put("type", ["Create", "Announce", "Follow"])
+
ActivityPub.fetch_public_activities(opts)
|> activities_to_statuses(%{for: user})
end
def fetch_user_statuses(user, opts \\ %{}) do
- ActivityPub.fetch_activities([], opts)
+ opts = opts
+ |> Map.put("type", ["Create"])
+ ActivityPub.fetch_public_activities(opts)
|> activities_to_statuses(%{for: user})
end
@@ -43,7 +55,7 @@ def fetch_mentions(user, opts \\ %{}) do
def fetch_conversation(user, id) do
with context when is_binary(context) <- conversation_id_to_context(id),
- activities <- ActivityPub.fetch_activities_for_context(context, %{"blocking_user" => user}),
+ activities <- ActivityPub.fetch_activities_for_context(context, %{"blocking_user" => user, "user" => user}),
statuses <- activities |> activities_to_statuses(%{for: user})
do
statuses
@@ -53,7 +65,8 @@ def fetch_conversation(user, id) do
end
def fetch_status(user, id) do
- with %Activity{} = activity <- Repo.get(Activity, id) do
+ with %Activity{} = activity <- Repo.get(Activity, id),
+ true <- ActivityPub.visible_for_user?(activity, user) do
activity_to_status(activity, %{for: user})
end
end
@@ -276,7 +289,7 @@ defp activity_to_status(activity, opts) do
actor = get_in(activity.data, ["actor"])
user = User.get_cached_by_ap_id(actor)
# mentioned_users = Repo.all(from user in User, where: user.ap_id in ^activity.data["to"])
- mentioned_users = Enum.map(activity.data["to"] || [], fn (ap_id) ->
+ mentioned_users = Enum.map(activity.recipients || [], fn (ap_id) ->
if ap_id do
User.get_cached_by_ap_id(ap_id)
else
diff --git a/lib/pleroma/web/twitter_api/twitter_api_controller.ex b/lib/pleroma/web/twitter_api/twitter_api_controller.ex
index 5284a8847..848ec218f 100644
--- a/lib/pleroma/web/twitter_api/twitter_api_controller.ex
+++ b/lib/pleroma/web/twitter_api/twitter_api_controller.ex
@@ -207,7 +207,8 @@ def register(conn, params) do
def update_avatar(%{assigns: %{user: user}} = conn, params) do
{:ok, object} = ActivityPub.upload(params)
change = Changeset.change(user, %{avatar: object.data})
- {:ok, user} = Repo.update(change)
+ {:ok, user} = User.update_and_set_cache(change)
+ CommonAPI.update(user)
render(conn, UserView, "show.json", %{user: user, for: user})
end
@@ -216,7 +217,8 @@ 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} <- Repo.update(change) do
+ {:ok, user} <- User.update_and_set_cache(change) do
+ CommonAPI.update(user)
%{"url" => [ %{ "href" => href } | _ ]} = object.data
response = %{ url: href } |> Poison.encode!
conn
@@ -228,7 +230,7 @@ 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} <- Repo.update(change) do
+ {:ok, _user} <- User.update_and_set_cache(change) do
%{"url" => [ %{ "href" => href } | _ ]} = object.data
response = %{ url: href } |> Poison.encode!
conn
@@ -255,7 +257,7 @@ def update_most_recent_notification(%{assigns: %{user: user}} = conn, %{"id" =>
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} <- Repo.update(changeset) do
+ {:ok, _user} <- User.update_and_set_cache(changeset) do
conn
|> json_reply(200, Poison.encode!(mrn))
else
@@ -305,7 +307,8 @@ def update_profile(%{assigns: %{user: user}} = conn, params) do
end
with changeset <- User.update_changeset(user, params),
- {:ok, user} <- Repo.update(changeset) do
+ {:ok, user} <- User.update_and_set_cache(changeset) do
+ CommonAPI.update(user)
render(conn, UserView, "user.json", %{user: user, for: user})
else
error ->
diff --git a/lib/pleroma/web/web_finger/web_finger.ex b/lib/pleroma/web/web_finger/web_finger.ex
index 95e717b17..c59a7e82d 100644
--- a/lib/pleroma/web/web_finger/web_finger.ex
+++ b/lib/pleroma/web/web_finger/web_finger.ex
@@ -45,6 +45,7 @@ def represent_user(user) do
{:Link, %{rel: "http://webfinger.net/rel/profile-page", type: "text/html", href: user.ap_id}},
{:Link, %{rel: "salmon", href: OStatus.salmon_path(user)}},
{:Link, %{rel: "magic-public-key", href: "data:application/magic-public-key,#{magic_key}"}},
+ {:Link, %{rel: "self", type: "application/activity+json", href: user.ap_id}},
{:Link, %{rel: "http://ostatus.org/schema/1.0/subscribe", template: OStatus.remote_follow_path()}}
]
}
@@ -59,7 +60,8 @@ def ensure_keys_present(user) do
else
{:ok, pem} = Salmon.generate_rsa_pem
info = Map.put(info, "keys", pem)
- Repo.update(Ecto.Changeset.change(user, info: info))
+ Ecto.Changeset.change(user, info: info)
+ |> User.update_and_set_cache()
end
end
@@ -70,12 +72,14 @@ defp webfinger_from_xml(doc) do
subject = XML.string_from_xpath("//Subject", doc)
salmon = XML.string_from_xpath(~s{//Link[@rel="salmon"]/@href}, doc)
subscribe_address = XML.string_from_xpath(~s{//Link[@rel="http://ostatus.org/schema/1.0/subscribe"]/@template}, doc)
+ ap_id = XML.string_from_xpath(~s{//Link[@rel="self" and @type="application/activity+json"]/@href}, doc)
data = %{
"magic_key" => magic_key,
"topic" => topic,
"subject" => subject,
"salmon" => salmon,
- "subscribe_address" => subscribe_address
+ "subscribe_address" => subscribe_address,
+ "ap_id" => ap_id
}
{:ok, data}
end
@@ -102,6 +106,7 @@ def find_lrdd_template(domain) do
end
def finger(account) do
+ account = String.trim_leading(account, "@")
domain = with [_name, domain] <- String.split(account, "@") do
domain
else _e ->
diff --git a/lib/pleroma/web/websub/websub.ex b/lib/pleroma/web/websub/websub.ex
index db1577a93..47a01849d 100644
--- a/lib/pleroma/web/websub/websub.ex
+++ b/lib/pleroma/web/websub/websub.ex
@@ -38,7 +38,15 @@ def verify(subscription, getter \\ &@httpoison.get/3) do
end
end
- def publish(topic, user, activity) do
+ @supported_activities [
+ "Create",
+ "Follow",
+ "Like",
+ "Announce",
+ "Undo",
+ "Delete"
+ ]
+ def publish(topic, user, %{data: %{"type" => type}} = activity) when type in @supported_activities do
# TODO: Only send to still valid subscriptions.
query = from sub in WebsubServerSubscription,
where: sub.topic == ^topic and sub.state == "active"
@@ -58,6 +66,7 @@ def publish(topic, user, activity) do
Pleroma.Web.Federator.enqueue(:publish_single_websub, data)
end)
end
+ def publish(_,_,_), do: ""
def sign(secret, doc) do
:crypto.hmac(:sha, secret, to_string(doc)) |> Base.encode16 |> String.downcase
diff --git a/priv/repo/migrations/20171212163643_add_recipients_to_activities.exs b/priv/repo/migrations/20171212163643_add_recipients_to_activities.exs
new file mode 100644
index 000000000..7bce78108
--- /dev/null
+++ b/priv/repo/migrations/20171212163643_add_recipients_to_activities.exs
@@ -0,0 +1,11 @@
+defmodule Pleroma.Repo.Migrations.AddRecipientsToActivities do
+ use Ecto.Migration
+
+ def change do
+ alter table(:activities) do
+ add :recipients, {:array, :string}
+ end
+
+ create index(:activities, [:recipients], using: :gin)
+ end
+end
diff --git a/priv/repo/migrations/20171212164525_fill_recipients_in_activities.exs b/priv/repo/migrations/20171212164525_fill_recipients_in_activities.exs
new file mode 100644
index 000000000..1fcc0dabb
--- /dev/null
+++ b/priv/repo/migrations/20171212164525_fill_recipients_in_activities.exs
@@ -0,0 +1,21 @@
+defmodule Pleroma.Repo.Migrations.FillRecipientsInActivities do
+ use Ecto.Migration
+ alias Pleroma.{Repo, Activity}
+
+ def up do
+ max = Repo.aggregate(Activity, :max, :id)
+ if max do
+ IO.puts("#{max} activities")
+ chunks = 0..(round(max / 10_000))
+
+ Enum.each(chunks, fn (i) ->
+ min = i * 10_000
+ max = min + 10_000
+ execute("""
+ update activities set recipients = array(select jsonb_array_elements_text(data->'to')) where id > #{min} and id <= #{max};
+ """)
+ |> IO.inspect
+ end)
+ end
+ end
+end
diff --git a/priv/repo/migrations/20180221210540_make_following_postgres_array.exs b/priv/repo/migrations/20180221210540_make_following_postgres_array.exs
new file mode 100644
index 000000000..98ca7d9d7
--- /dev/null
+++ b/priv/repo/migrations/20180221210540_make_following_postgres_array.exs
@@ -0,0 +1,18 @@
+defmodule Pleroma.Repo.Migrations.MakeFollowingPostgresArray do
+ use Ecto.Migration
+
+ def change do
+ alter table(:users) do
+ add :following_temp, {:array, :string}
+ end
+
+ execute """
+ update users set following_temp = array(select jsonb_array_elements_text(following));
+ """
+
+ alter table(:users) do
+ remove :following
+ end
+ rename table(:users), :following_temp, to: :following
+ end
+end
diff --git a/test/fixtures/avatar_data_uri b/test/fixtures/avatar_data_uri
new file mode 100644
index 000000000..49a8b6404
--- /dev/null
+++ b/test/fixtures/avatar_data_uri
@@ -0,0 +1 @@
+data:image/jpeg;base64,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
diff --git a/test/fixtures/httpoison_mock/7369654.atom b/test/fixtures/httpoison_mock/7369654.atom
new file mode 100644
index 000000000..74fd9ce6b
--- /dev/null
+++ b/test/fixtures/httpoison_mock/7369654.atom
@@ -0,0 +1,44 @@
+
+
http://mastodon.example.org/media/hw4nrZmV5DPbW2z_hao
", + "cc": [ + "http://mastodon.example.org/users/admin/followers" + ], + "attributedTo": "http://mastodon.example.org/users/admin", + "attachment": [ + { + "url": "http://mastodon.example.org/system/media_attachments/files/000/000/002/original/334ce029e7bfb920.jpg", + "type": "Document", + "name": null, + "mediaType": "image/jpeg" + } + ], + "atomUri": "http://mastodon.example.org/users/admin/statuses/99541822081679796" + }, + "id": "http://mastodon.example.org/users/admin/statuses/99541822081679796/activity", + "cc": [ + "http://mastodon.example.org/users/admin/followers" + ], + "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" + } + ] +} \ No newline at end of file diff --git a/test/fixtures/mastodon-delete.json b/test/fixtures/mastodon-delete.json new file mode 100644 index 000000000..87a582002 --- /dev/null +++ b/test/fixtures/mastodon-delete.json @@ -0,0 +1,33 @@ +{ + "type": "Delete", + "signature": { + "type": "RsaSignature2017", + "signatureValue": "cw0RlfNREf+5VdsOYcCBDrv521eiLsDTAYNHKffjF0bozhCnOh+wHkFik7WamUk$ +uEiN4L2H6vPlGRprAZGRhEwgy+A7rIFQNmLrpW5qV5UNVI/2F7kngEHqZQgbQYj9hW+5GMYmPkHdv3D72ZefGw$ +4Xa2NBLGFpAjQllfzt7kzZLKKY2DM99FdUa64I2Wj3iD04Hs23SbrUdAeuGk/c1Cg6bwGNG4vxoiwn1jikgJLA$ +NAlSGjsRGdR7LfbC7GqWWsW3cSNsLFPoU6FyALjgTrrYoHiXe0QHggw+L3yMLfzB2S/L46/VRbyb+WDKMBIXUL$ +5owmzHSi6e/ZtCI3w==", + "creator": "http://mastodon.example.org/users/gargron#main-key", "created": "2018-03-03T16:24:11Z" + }, + "object": { + "type": "Tombstone", + "id": "http://mastodon.example.org/users/gargron/statuses/99620895606148759", + "atomUri": "http://mastodon.example.org/users/gargron/statuses/99620895606148759" + }, + "id": "http://mastodon.example.org/users/gargron/statuses/99620895606148759#delete", + "actor": "http://mastodon.example.org/users/gargron", + "@context": [ + { + "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-follow-activity.json b/test/fixtures/mastodon-follow-activity.json new file mode 100644 index 000000000..7a6de6996 --- /dev/null +++ b/test/fixtures/mastodon-follow-activity.json @@ -0,0 +1,29 @@ +{ + "type": "Follow", + "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" + } + ] +} \ No newline at end of file diff --git a/test/fixtures/mastodon-like.json b/test/fixtures/mastodon-like.json new file mode 100644 index 000000000..39fb44c4a --- /dev/null +++ b/test/fixtures/mastodon-like.json @@ -0,0 +1,29 @@ +{ + "type": "Like", + "signature": { + "type": "RsaSignature2017", + "signatureValue": "fdxMfQSMwbC6wP6sh6neS/vM5879K67yQkHTbiT5Npr5wAac0y6+o3Ij+41tN3rL6wfuGTosSBTHOtta6R4GCOOhCaCSLMZKypnp1VltCzLDoyrZELnYQIC8gpUXVmIycZbREk22qWUe/w7DAFaKK4UscBlHDzeDVcA0K3Se5Sluqi9/Zh+ldAnEzj/rSEPDjrtvf5wGNf3fHxbKSRKFt90JvKK6hS+vxKUhlRFDf6/SMETw+EhwJSNW4d10yMUakqUWsFv4Acq5LW7l+HpYMvlYY1FZhNde1+uonnCyuQDyvzkff8zwtEJmAXC4RivO/VVLa17SmqheJZfI8oluVg==", + "creator": "http://mastodon.example.org/users/admin#main-key", + "created": "2018-02-17T18:57:49Z" + }, + "object": "http://localtesting.pleroma.lol/objects/eb92579d-3417-42a8-8652-2492c2d4f454", + "nickname": "lain", + "id": "http://mastodon.example.org/users/admin#likes/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" + } + ] +} \ No newline at end of file diff --git a/test/fixtures/mastodon-note-object.json b/test/fixtures/mastodon-note-object.json new file mode 100644 index 000000000..75bed9625 --- /dev/null +++ b/test/fixtures/mastodon-note-object.json @@ -0,0 +1,9 @@ +{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1",{"manuallyApprovesFollowers":"as:manuallyApprovesFollowers","sensitive":"as:sensitive","movedTo":"as:movedTo","Hashtag":"as:Hashtag","ostatus":"http://ostatus.org#","atomUri":"ostatus:atomUri","inReplyToAtomUri":"ostatus:inReplyToAtomUri","conversation":"ostatus:conversation","toot":"http://joinmastodon.org/ns#","Emoji":"toot:Emoji"}],"id":"http://mastodon.example.org/users/admin/statuses/99541947525187367","type":"Note","summary":null,"content":"\u003cp\u003eyeah.\u003c/p\u003e","inReplyTo":null,"published":"2018-02-17T17:46:20Z","url":"http://mastodon.example.org/@admin/99541947525187367","attributedTo":"http://mastodon.example.org/users/admin","to":["https://www.w3.org/ns/activitystreams#Public"],"cc":["http://mastodon.example.org/users/admin/followers"],"sensitive":false,"atomUri":"http://mastodon.example.org/users/admin/statuses/99541947525187367","inReplyToAtomUri":null,"conversation":"tag:mastodon.example.org,2018-02-17:objectId=59:objectType=Conversation","tag":[], + "attachment": [ + { + "url": "http://mastodon.example.org/system/media_attachments/files/000/000/002/original/334ce029e7bfb920.jpg", + "type": "Document", + "name": null, + "mediaType": "image/jpeg" + } + ]} diff --git a/test/fixtures/mastodon-note-unlisted.xml b/test/fixtures/mastodon-note-unlisted.xml new file mode 100644 index 000000000..d21017b80 --- /dev/null +++ b/test/fixtures/mastodon-note-unlisted.xml @@ -0,0 +1,38 @@ + +Some bio
", + "publicKey": { + "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0gs3VnQf6am3R+CeBV4H\nlfI1HZTNRIBHgvFszRZkCERbRgEWMu+P+I6/7GJC5H5jhVQ60z4MmXcyHOGmYMK/\n5XyuHQz7V2Ssu1AxLfRN5Biq1ayb0+DT/E7QxNXDJPqSTnstZ6C7zKH/uAETqg3l\nBonjCQWyds+IYbQYxf5Sp3yhvQ80lMwHML3DaNCMlXWLoOnrOX5/yK5+dedesg2\n/HIvGk+HEt36vm6hoH7bwPuEkgA++ACqwjXRe5Mta7i3eilHxFaF8XIrJFARV0t\nqOu4GID/jG6oA+swIWndGrtR2QRJIt9QIBFfK3HG5M0koZbY1eTqwNFRHFL3xaD\nUQIDAQAB\n-----END PUBLIC KEY-----\n", + "owner": "http://mastodon.example.org/users/gargron", + "id": "http://mastodon.example.org/users/gargron#main-key" + }, + "preferredUsername": "gargron", + "outbox": "http://mastodon.example.org/users/gargron/outbox", + "name": "gargle", + "manuallyApprovesFollowers": false, + "inbox": "http://mastodon.example.org/users/gargron/inbox", + "id": "http://mastodon.example.org/users/gargron", + "following": "http://mastodon.example.org/users/gargron/following", + "followers": "http://mastodon.example.org/users/gargron/followers", + "endpoints": { + "sharedInbox": "http://mastodon.example.org/inbox" + }, + "icon":{"type":"Image","mediaType":"image/jpeg","url":"https://cd.niu.moe/accounts/avatars/000/033/323/original/fd7f8ae0b3ffedc9.jpeg"},"image":{"type":"Image","mediaType":"image/png","url":"https://cd.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png"} + }, + "id": "http://mastodon.example.org/users/gargron#updates/1519563538", + "actor": "http://mastodon.example.org/users/gargron", + "@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/support/builders/activity_builder.ex b/test/support/builders/activity_builder.ex index 7ce611f8f..0ebf633b3 100644 --- a/test/support/builders/activity_builder.ex +++ b/test/support/builders/activity_builder.ex @@ -8,9 +8,11 @@ def build(data \\ %{}, opts \\ %{}) do "id" => Pleroma.Web.ActivityPub.Utils.generate_object_id, "actor" => user.ap_id, "to" => ["https://www.w3.org/ns/activitystreams#Public"], + "type" => "Create", "object" => %{ "type" => "Note", - "content" => "test" + "content" => "test", + "to" => ["https://www.w3.org/ns/activitystreams#Public"], } } Map.merge(activity, data) @@ -23,7 +25,7 @@ def insert(data \\ %{}, opts \\ %{}) do def insert_list(times, data \\ %{}, opts \\ %{}) do Enum.map(1..times, fn (n) -> - {:ok, activity} = insert(data) + {:ok, activity} = insert(data, opts) activity end) end @@ -32,7 +34,7 @@ def public_and_non_public do user = Pleroma.Factory.insert(:user) public = build(%{"id" => 1}, %{user: user}) - non_public = build(%{"id" => 2, "to" => []}, %{user: user}) + non_public = build(%{"id" => 2, "to" => [user.follower_address]}, %{user: user}) {:ok, public} = ActivityPub.insert(public) {:ok, non_public} = ActivityPub.insert(non_public) diff --git a/test/support/builders/user_builder.ex b/test/support/builders/user_builder.ex index 710a1b87c..1e1e80ac9 100644 --- a/test/support/builders/user_builder.ex +++ b/test/support/builders/user_builder.ex @@ -14,6 +14,8 @@ def build(data \\ %{}) do end def insert(data \\ %{}) do - Repo.insert(build(data)) + {:ok, user} = Repo.insert(build(data)) + User.invalidate_cache(user) + {:ok, user} end end diff --git a/test/support/factory.ex b/test/support/factory.ex index 7f378915e..1445fe828 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -52,7 +52,8 @@ def note_activity_factory do %Pleroma.Activity{ data: data, - actor: data["actor"] + actor: data["actor"], + recipients: data["to"] } end diff --git a/test/support/httpoison_mock.ex b/test/support/httpoison_mock.ex index 21607ba95..ad9be9aef 100644 --- a/test/support/httpoison_mock.ex +++ b/test/support/httpoison_mock.ex @@ -80,6 +80,13 @@ def get("https://shitposter.club/.well-known/webfinger?resource=https://shitpost }} end + def get("https://shitposter.club/.well-known/webfinger?resource=https://shitposter.club/user/5381", [Accept: "application/xrd+xml"], []) do + {:ok, %Response{ + status_code: 200, + body: File.read!("test/fixtures/httpoison_mock/spc_5381_xrd.xml") + }} + end + def get("http://gs.example.org/.well-known/webfinger", [Accept: "application/xrd+xml"], [params: [resource: "http://gs.example.org:4040/index.php/user/1"], follow_redirect: true]) do {:ok, %Response{ status_code: 200, @@ -122,6 +129,13 @@ def get("https://social.heldscal.la/api/statuses/user_timeline/29191.atom", _bod }} end + def get("https://shitposter.club/api/statuses/user_timeline/5381.atom", _body, _headers) do + {:ok, %Response{ + status_code: 200, + body: File.read!("test/fixtures/httpoison_mock/spc_5381.atom") + }} + end + def get("https://social.heldscal.la/api/statuses/user_timeline/23211.atom", _body, _headers) do {:ok, %Response{ status_code: 200, @@ -366,6 +380,62 @@ def get("http://social.stopwatchingus-heidelberg.de/.well-known/host-meta", [], }} end + def get("http://mastodon.example.org/users/admin", ["Accept": "application/activity+json"], _) do + {:ok, %Response{ + status_code: 200, + body: File.read!("test/fixtures/httpoison_mock/admin@mastdon.example.org.json") + }} + end + + def get("https://masto.quad.moe/users/_HellPie", ["Accept": "application/activity+json"], _) do + {:ok, %Response{ + status_code: 200, + body: File.read!("test/fixtures/httpoison_mock/hellpie.json") + }} + end + + def get("https://niu.moe/users/rye", ["Accept": "application/activity+json"], _) do + {:ok, %Response{ + status_code: 200, + body: File.read!("test/fixtures/httpoison_mock/rye.json") + }} + end + + def get("https://mstdn.io/users/mayuutann", ["Accept": "application/activity+json"], _) do + {:ok, %Response{ + status_code: 200, + body: File.read!("test/fixtures/httpoison_mock/mayumayu.json") + }} + end + + def get("http://mastodon.example.org/@admin/99541947525187367", ["Accept": "application/activity+json"], _) do + {:ok, %Response{ + status_code: 200, + body: File.read!("test/fixtures/mastodon-note-object.json") + }} + end + + def get("https://mstdn.io/users/mayuutann/statuses/99568293732299394", ["Accept": "application/activity+json"], _) do + {:ok, %Response{ + status_code: 200, + body: File.read!("test/fixtures/httpoison_mock/mayumayupost.json") + }} + end + + def get("https://shitposter.club/notice/7369654", _, _) do + {:ok, %Response{ + status_code: 200, + body: File.read!("test/fixtures/httpoison_mock/7369654.html") + }} + end + + def get("https://shitposter.club/api/statuses/show/7369654.atom", _body, _headers) do + {:ok, %Response{ + status_code: 200, + body: File.read!("test/fixtures/httpoison_mock/7369654.atom") + }} + end + def get(url, body, headers) do {:error, "Not implemented the mock response for get #{inspect(url)}, #{inspect(body)}, #{inspect(headers)}"} end diff --git a/test/user_test.exs b/test/user_test.exs index 6bfdfe1bf..35de0a7ce 100644 --- a/test/user_test.exs +++ b/test/user_test.exs @@ -46,21 +46,22 @@ test "can't follow a deactivated users" do {:error, _} = User.follow(user, followed) end - test "following a remote user will ensure a websub subscription is present" do - user = insert(:user) - {:ok, followed} = OStatus.make_user("shp@social.heldscal.la") + # This is a somewhat useless test. + # test "following a remote user will ensure a websub subscription is present" do + # user = insert(:user) + # {:ok, followed} = OStatus.make_user("shp@social.heldscal.la") - assert followed.local == false + # assert followed.local == false - {:ok, user} = User.follow(user, followed) - assert User.ap_followers(followed) in user.following + # {:ok, user} = User.follow(user, followed) + # assert User.ap_followers(followed) in user.following - query = from w in WebsubClientSubscription, - where: w.topic == ^followed.info["topic"] - websub = Repo.one(query) + # query = from w in WebsubClientSubscription, + # where: w.topic == ^followed.info["topic"] + # websub = Repo.one(query) - assert websub - end + # assert websub + # end test "unfollow takes a user and another user" do followed = insert(:user) @@ -371,4 +372,15 @@ test ".delete deactivates a user, all follow relationships and all create activi refute Repo.get(Activity, activity.id) end + + test "get_public_key_for_ap_id fetches a user that's not in the db" do + assert {:ok, _key} = User.get_public_key_for_ap_id("http://mastodon.example.org/users/admin") + end + + test "insert or update a user from given data" do + user = insert(:user, %{nickname: "nick@name.de"}) + data = %{ ap_id: user.ap_id <> "xxx", name: user.name, nickname: user.nickname } + + assert {:ok, %User{}} = User.insert_or_update_user(data) + end end diff --git a/test/web/activity_pub/activity_pub_controller_test.exs b/test/web/activity_pub/activity_pub_controller_test.exs new file mode 100644 index 000000000..c8082cd03 --- /dev/null +++ b/test/web/activity_pub/activity_pub_controller_test.exs @@ -0,0 +1,49 @@ +defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do + use Pleroma.Web.ConnCase + import Pleroma.Factory + alias Pleroma.Web.ActivityPub.{UserView, ObjectView} + alias Pleroma.{Repo, User} + alias Pleroma.Activity + + describe "/users/:nickname" do + test "it returns a json representation of the user", %{conn: conn} do + user = insert(:user) + + conn = conn + |> put_req_header("accept", "application/activity+json") + |> get("/users/#{user.nickname}") + + user = Repo.get(User, user.id) + + assert json_response(conn, 200) == UserView.render("user.json", %{user: user}) + end + end + + describe "/object/:uuid" do + test "it returns a json representation of the object", %{conn: conn} do + note = insert(:note) + uuid = String.split(note.data["id"], "/") |> List.last + + conn = conn + |> put_req_header("accept", "application/activity+json") + |> get("/objects/#{uuid}") + + assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note}) + end + end + + describe "/users/:nickname/inbox" do + test "it inserts an incoming activity into the database", %{conn: conn} do + data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode! + + conn = conn + |> assign(:valid_signature, true) + |> put_req_header("content-type", "application/activity+json") + |> post("/inbox", data) + + assert "ok" == json_response(conn, 200) + :timer.sleep(500) + assert Activity.get_by_ap_id(data["id"]) + end + end +end diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs index f423ea8be..96792bca5 100644 --- a/test/web/activity_pub/activity_pub_test.exs +++ b/test/web/activity_pub/activity_pub_test.exs @@ -7,6 +7,18 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do import Pleroma.Factory + describe "building a user from his ap id" do + test "it returns a user" do + user_id = "http://mastodon.example.org/users/admin" + {:ok, user} = ActivityPub.make_user_from_ap_id(user_id) + assert user.ap_id == user_id + assert user.nickname == "admin@mastodon.example.org" + assert user.info["source_data"] + assert user.info["ap_enabled"] + assert user.follower_address == "http://mastodon.example.org/users/admin/followers" + end + end + describe "insertion" do test "returns the activity if one with the same id is already in" do activity = insert(:note_activity) @@ -35,9 +47,10 @@ test "inserts a given map into the activity database, giving it an id if it has assert activity.data["id"] == given_id end - test "adds an id to a given object if it lacks one and inserts it to the object database" do + test "adds an id to a given object if it lacks one and is a note and inserts it to the object database" do data = %{ "object" => %{ + "type" => "Note", "ok" => true } } @@ -50,9 +63,10 @@ test "adds an id to a given object if it lacks one and inserts it to the object describe "create activities" do test "removes doubled 'to' recipients" do - {:ok, activity} = ActivityPub.create(["user1", "user1", "user2"], %User{ap_id: "1"}, "", %{}) + {:ok, activity} = ActivityPub.create(%{to: ["user1", "user1", "user2"], actor: %User{ap_id: "1"}, context: "", object: %{}}) assert activity.data["to"] == ["user1", "user2"] assert activity.actor == "1" + assert activity.recipients == ["user1", "user2"] end end @@ -252,6 +266,39 @@ test "fetches the latest Follow activity" do end end + describe "fetching an object" do + test "it fetches an object" do + {:ok, object} = ActivityPub.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367") + assert activity = Activity.get_create_activity_by_object_ap_id(object.data["id"]) + assert activity.data["id"] + + {:ok, object_again} = ActivityPub.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367") + + assert [attachment] = object.data["attachment"] + assert is_list(attachment["url"]) + + assert object == object_again + end + + test "it works with objects only available via Ostatus" do + {:ok, object} = ActivityPub.fetch_object_from_id("https://shitposter.club/notice/2827873") + assert activity = Activity.get_create_activity_by_object_ap_id(object.data["id"]) + assert activity.data["id"] + + {:ok, object_again} = ActivityPub.fetch_object_from_id("https://shitposter.club/notice/2827873") + + assert object == object_again + end + + test "it correctly stitches up conversations between ostatus and ap" do + last = "https://mstdn.io/users/mayuutann/statuses/99568293732299394" + {:ok, object} = ActivityPub.fetch_object_from_id(last) + + object = Object.get_by_ap_id(object.data["inReplyTo"]) + assert object + end + end + describe "following / unfollowing" do test "creates a follow activity" do follower = insert(:user) @@ -292,7 +339,21 @@ test "it creates a delete activity and deletes the original object" do end end + describe "update" do + test "it creates an update activity with the new user data" do + user = insert(:user) + {:ok, user} = Pleroma.Web.WebFinger.ensure_keys_present(user) + user_data = Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user}) + {:ok, update} = ActivityPub.update(%{actor: user_data["id"], to: [user.follower_address], cc: [], object: user_data}) + + assert update.data["actor"] == user.ap_id + assert update.data["to"] == [user.follower_address] + assert update.data["object"]["id"] == user_data["id"] + assert update.data["object"]["type"] == user_data["type"] + end + end + def data_uri do - "data:image/jpeg;base64,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" + File.read!("test/fixtures/avatar_data_uri") end end diff --git a/test/web/activity_pub/transmogrifier_test.exs b/test/web/activity_pub/transmogrifier_test.exs new file mode 100644 index 000000000..c0ee209f1 --- /dev/null +++ b/test/web/activity_pub/transmogrifier_test.exs @@ -0,0 +1,277 @@ +defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do + use Pleroma.DataCase + alias Pleroma.Web.ActivityPub.Transmogrifier + 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 + 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 == "Some bio
" + 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 + 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 + 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 diff --git a/test/web/activity_pub/views/object_view_test.exs b/test/web/activity_pub/views/object_view_test.exs new file mode 100644 index 000000000..6a1311be7 --- /dev/null +++ b/test/web/activity_pub/views/object_view_test.exs @@ -0,0 +1,17 @@ +defmodule Pleroma.Web.ActivityPub.ObjectViewTest do + use Pleroma.DataCase + import Pleroma.Factory + + alias Pleroma.Web.ActivityPub.ObjectView + + test "renders a note object" do + note = insert(:note) + + result = ObjectView.render("object.json", %{object: note}) + + assert result["id"] == note.data["id"] + assert result["to"] == note.data["to"] + assert result["content"] == note.data["content"] + assert result["type"] == "Note" + end +end diff --git a/test/web/activity_pub/views/user_view_test.exs b/test/web/activity_pub/views/user_view_test.exs new file mode 100644 index 000000000..0c64e62c3 --- /dev/null +++ b/test/web/activity_pub/views/user_view_test.exs @@ -0,0 +1,18 @@ +defmodule Pleroma.Web.ActivityPub.UserViewTest do + use Pleroma.DataCase + import Pleroma.Factory + + alias Pleroma.Web.ActivityPub.UserView + + test "Renders a user, including the public key" do + user = insert(:user) + {:ok, user} = Pleroma.Web.WebFinger.ensure_keys_present(user) + + result = UserView.render("user.json", %{user: user}) + + assert result["id"] == user.ap_id + assert result["preferredUsername"] == user.nickname + + assert String.contains?(result["publicKey"]["publicKeyPem"], "BEGIN RSA PUBLIC KEY") + end +end diff --git a/test/web/http_sigs/http_sig_test.exs b/test/web/http_sigs/http_sig_test.exs new file mode 100644 index 000000000..fd568a67a --- /dev/null +++ b/test/web/http_sigs/http_sig_test.exs @@ -0,0 +1,154 @@ +# http signatures +# Test data from https://tools.ietf.org/html/draft-cavage-http-signatures-08#appendix-C +defmodule Pleroma.Web.HTTPSignaturesTest do + use Pleroma.DataCase + alias Pleroma.Web.HTTPSignatures + import Pleroma.Factory + + @private_key (hd(:public_key.pem_decode(File.read!("test/web/http_sigs/priv.key"))) + |> :public_key.pem_entry_decode()) + + @public_key (hd(:public_key.pem_decode(File.read!("test/web/http_sigs/pub.key"))) + |> :public_key.pem_entry_decode()) + + @headers %{ + "(request-target)" => "post /foo?param=value&pet=dog", + "host" => "example.com", + "date" => "Thu, 05 Jan 2014 21:31:40 GMT", + "content-type" => "application/json", + "digest" => "SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=", + "content-length" => "18" + } + + @body "{\"hello\": \"world\"}" + + @default_signature """ + keyId="Test",algorithm="rsa-sha256",signature="jKyvPcxB4JbmYY4mByyBY7cZfNl4OW9HpFQlG7N4YcJPteKTu4MWCLyk+gIr0wDgqtLWf9NLpMAMimdfsH7FSWGfbMFSrsVTHNTk0rK3usrfFnti1dxsM4jl0kYJCKTGI/UWkqiaxwNiKqGcdlEDrTcUhhsFsOIo8VhddmZTZ8w=" + """ + + @basic_signature """ + keyId="Test",algorithm="rsa-sha256",headers="(request-target) host date",signature="HUxc9BS3P/kPhSmJo+0pQ4IsCo007vkv6bUm4Qehrx+B1Eo4Mq5/6KylET72ZpMUS80XvjlOPjKzxfeTQj4DiKbAzwJAb4HX3qX6obQTa00/qPDXlMepD2JtTw33yNnm/0xV7fQuvILN/ys+378Ysi082+4xBQFwvhNvSoVsGv4=" + """ + + @all_headers_signature """ + keyId="Test",algorithm="rsa-sha256",headers="(request-target) host date content-type digest content-length",signature="Ef7MlxLXoBovhil3AlyjtBwAL9g4TN3tibLj7uuNB3CROat/9KaeQ4hW2NiJ+pZ6HQEOx9vYZAyi+7cmIkmJszJCut5kQLAwuX+Ms/mUFvpKlSo9StS2bMXDBNjOh4Auj774GFj4gwjS+3NhFeoqyr/MuN6HsEnkvn6zdgfE2i0=" + """ + + test "split up a signature" do + expected = %{ + "keyId" => "Test", + "algorithm" => "rsa-sha256", + "signature" => "jKyvPcxB4JbmYY4mByyBY7cZfNl4OW9HpFQlG7N4YcJPteKTu4MWCLyk+gIr0wDgqtLWf9NLpMAMimdfsH7FSWGfbMFSrsVTHNTk0rK3usrfFnti1dxsM4jl0kYJCKTGI/UWkqiaxwNiKqGcdlEDrTcUhhsFsOIo8VhddmZTZ8w=", + "headers" => ["date"] + } + + assert HTTPSignatures.split_signature(@default_signature) == expected + end + + test "validates the default case" do + signature = HTTPSignatures.split_signature(@default_signature) + assert HTTPSignatures.validate(@headers, signature, @public_key) + end + + test "validates the basic case" do + signature = HTTPSignatures.split_signature(@basic_signature) + assert HTTPSignatures.validate(@headers, signature, @public_key) + end + + test "validates the all-headers case" do + signature = HTTPSignatures.split_signature(@all_headers_signature) + assert HTTPSignatures.validate(@headers, signature, @public_key) + end + + test "it contructs a signing string" do + expected = "date: Thu, 05 Jan 2014 21:31:40 GMT\ncontent-length: 18" + assert expected == HTTPSignatures.build_signing_string(@headers, ["date", "content-length"]) + end + + test "it validates a conn" do + public_key_pem = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnGb42rPZIapY4Hfhxrgn\nxKVJczBkfDviCrrYaYjfGxawSw93dWTUlenCVTymJo8meBlFgIQ70ar4rUbzl6GX\nMYvRdku072d1WpglNHXkjKPkXQgngFDrh2sGKtNB/cEtJcAPRO8OiCgPFqRtMiNM\nc8VdPfPdZuHEIZsJ/aUM38EnqHi9YnVDQik2xxDe3wPghOhqjxUM6eLC9jrjI+7i\naIaEygUdyst9qVg8e2FGQlwAeS2Eh8ygCxn+bBlT5OyV59jSzbYfbhtF2qnWHtZy\nkL7KOOwhIfGs7O9SoR2ZVpTEQ4HthNzainIe/6iCR5HGrao/T8dygweXFYRv+k5A\nPQIDAQAB\n-----END PUBLIC KEY-----\n" + [public_key] = :public_key.pem_decode(public_key_pem) + + public_key = public_key + |> :public_key.pem_entry_decode() + + conn = %{ + req_headers: [ + {"host", "localtesting.pleroma.lol"}, + {"connection", "close"}, + {"content-length", "2316"}, + {"user-agent", "http.rb/2.2.2 (Mastodon/2.1.0.rc3; +http://mastodon.example.org/)"}, + {"date", "Sun, 10 Dec 2017 14:23:49 GMT"}, + {"digest", "SHA-256=x/bHADMW8qRrq2NdPb5P9fl0lYpKXXpe5h5maCIL0nM="}, + {"content-type", "application/activity+json"}, + {"(request-target)", "post /users/demiurge/inbox"}, + {"signature", "keyId=\"http://mastodon.example.org/users/admin#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) user-agent host date digest content-type\",signature=\"i0FQvr51sj9BoWAKydySUAO1RDxZmNY6g7M62IA7VesbRSdFZZj9/fZapLp6YSuvxUF0h80ZcBEq9GzUDY3Chi9lx6yjpUAS2eKb+Am/hY3aswhnAfYd6FmIdEHzsMrpdKIRqO+rpQ2tR05LwiGEHJPGS0p528NvyVxrxMT5H5yZS5RnxY5X2HmTKEgKYYcvujdv7JWvsfH88xeRS7Jlq5aDZkmXvqoR4wFyfgnwJMPLel8P/BUbn8BcXglH/cunR0LUP7sflTxEz+Rv5qg+9yB8zgBsB4C0233WpcJxjeD6Dkq0EcoJObBR56F8dcb7NQtUDu7x6xxzcgSd7dHm5w==\""}] + } + + assert HTTPSignatures.validate_conn(conn, public_key) + end + + test "it validates a conn and fetches the key" do + conn = %{ + params: %{"actor" => "http://mastodon.example.org/users/admin"}, + req_headers: [ + {"host", "localtesting.pleroma.lol"}, + {"x-forwarded-for", "127.0.0.1"}, + {"connection", "close"}, + {"content-length", "2307"}, + {"user-agent", "http.rb/2.2.2 (Mastodon/2.1.0.rc3; +http://mastodon.example.org/)"}, + {"date", "Sun, 11 Feb 2018 17:12:01 GMT"}, + {"digest", "SHA-256=UXsAnMtR9c7mi1FOf6HRMtPgGI1yi2e9nqB/j4rZ99I="}, + {"content-type", "application/activity+json"}, + {"signature", "keyId=\"http://mastodon.example.org/users/admin#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) user-agent host date digest content-type\",signature=\"qXKqpQXUpC3d9bZi2ioEeAqP8nRMD021CzH1h6/w+LRk4Hj31ARJHDwQM+QwHltwaLDUepshMfz2WHSXAoLmzWtvv7xRwY+mRqe+NGk1GhxVZ/LSrO/Vp7rYfDpfdVtkn36LU7/Bzwxvvaa4ZWYltbFsRBL0oUrqsfmJFswNCQIG01BB52BAhGSCORHKtQyzo1IZHdxl8y80pzp/+FOK2SmHkqWkP9QbaU1qTZzckL01+7M5btMW48xs9zurEqC2sM5gdWMQSZyL6isTV5tmkTZrY8gUFPBJQZgihK44v3qgfWojYaOwM8ATpiv7NG8wKN/IX7clDLRMA8xqKRCOKw==\""}, + {"(request-target)", "post /users/demiurge/inbox"} + ] + } + + assert HTTPSignatures.validate_conn(conn) + end + + test "validate this" do + conn = %{ + params: %{"actor" => "https://niu.moe/users/rye"}, + req_headers: [ + {"x-forwarded-for", "149.202.73.191"}, + {"host", "testing.pleroma.lol"}, + {"x-cluster-client-ip", "149.202.73.191"}, + {"connection", "upgrade"}, + {"content-length", "2396"}, + {"user-agent", "http.rb/3.0.0 (Mastodon/2.2.0; +https://niu.moe/)"}, + {"date", "Sun, 18 Feb 2018 20:31:51 GMT"}, + {"digest", "SHA-256=dzH+vLyhxxALoe9RJdMl4hbEV9bGAZnSfddHQzeidTU="}, + {"content-type", "application/activity+json"}, + {"signature", "keyId=\"https://niu.moe/users/rye#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) user-agent host date digest content-type\",signature=\"wtxDg4kIpW7nsnUcVJhBk6SgJeDZOocr8yjsnpDRqE52lR47SH6X7G16r7L1AUJdlnbfx7oqcvomoIJoHB3ghP6kRnZW6MyTMZ2jPoi3g0iC5RDqv6oAmDSO14iw6U+cqZbb3P/odS5LkbThF0UNXcfenVNfsKosIJycFjhNQc54IPCDXYq/7SArEKJp8XwEgzmiC2MdxlkVIUSTQYfjM4EG533cwlZocw1mw72e5mm/owTa80BUZAr0OOuhoWARJV9btMb02ZyAF6SCSoGPTA37wHyfM1Dk88NHf7Z0Aov/Fl65dpRM+XyoxdkpkrhDfH9qAx4iuV2VEWddQDiXHA==\""}, + {"(request-target)", "post /inbox"} + ] + } + assert HTTPSignatures.validate_conn(conn) + end + + test "validate this too" do + conn = %{ + params: %{"actor" => "https://niu.moe/users/rye"}, + req_headers: [ + {"x-forwarded-for", "149.202.73.191"}, + {"host", "testing.pleroma.lol"}, + {"x-cluster-client-ip", "149.202.73.191"}, + {"connection", "upgrade"}, + {"content-length", "2342"}, + {"user-agent", "http.rb/3.0.0 (Mastodon/2.2.0; +https://niu.moe/)"}, + {"date", "Sun, 18 Feb 2018 21:44:46 GMT"}, + {"digest", "SHA-256=vS8uDOJlyAu78cF3k5EzrvaU9iilHCX3chP37gs5sS8="}, + {"content-type", "application/activity+json"}, + {"signature", "keyId=\"https://niu.moe/users/rye#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) user-agent host date digest content-type\",signature=\"IN6fHD8pLiDEf35dOaRHzJKc1wBYh3/Yq0ItaNGxUSbJTd2xMjigZbcsVKzvgYYjglDDN+disGNeD+OBKwMqkXWaWe/lyMc9wHvCH5NMhpn/A7qGLY8yToSt4vh8ytSkZKO6B97yC+Nvy6Fz/yMbvKtFycIvSXCq417cMmY6f/aG+rtMUlTbKO5gXzC7SUgGJCtBPCh1xZzu5/w0pdqdjO46ePNeR6JyJSLLV4hfo3+p2n7SRraxM4ePVCUZqhwS9LPt3Zdhy3ut+IXCZgMVIZggQFM+zXLtcXY5HgFCsFQr5WQDu+YkhWciNWtKFnWfAsnsg5sC330lZ/0Z8Z91yA==\""}, + {"(request-target)", "post /inbox"} + ]} + assert HTTPSignatures.validate_conn(conn) + end + + test "it generates a signature" do + user = insert(:user) + assert HTTPSignatures.sign(user, %{host: "mastodon.example.org"}) =~ "keyId=\"" + end +end diff --git a/test/web/http_sigs/priv.key b/test/web/http_sigs/priv.key new file mode 100644 index 000000000..425518a06 --- /dev/null +++ b/test/web/http_sigs/priv.key @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXgIBAAKBgQDCFENGw33yGihy92pDjZQhl0C36rPJj+CvfSC8+q28hxA161QF +NUd13wuCTUcq0Qd2qsBe/2hFyc2DCJJg0h1L78+6Z4UMR7EOcpfdUE9Hf3m/hs+F +UR45uBJeDK1HSFHD8bHKD6kv8FPGfJTotc+2xjJwoYi+1hqp1fIekaxsyQIDAQAB +AoGBAJR8ZkCUvx5kzv+utdl7T5MnordT1TvoXXJGXK7ZZ+UuvMNUCdN2QPc4sBiA +QWvLw1cSKt5DsKZ8UETpYPy8pPYnnDEz2dDYiaew9+xEpubyeW2oH4Zx71wqBtOK +kqwrXa/pzdpiucRRjk6vE6YY7EBBs/g7uanVpGibOVAEsqH1AkEA7DkjVH28WDUg +f1nqvfn2Kj6CT7nIcE3jGJsZZ7zlZmBmHFDONMLUrXR/Zm3pR5m0tCmBqa5RK95u +412jt1dPIwJBANJT3v8pnkth48bQo/fKel6uEYyboRtA5/uHuHkZ6FQF7OUkGogc +mSJluOdc5t6hI1VsLn0QZEjQZMEOWr+wKSMCQQCC4kXJEsHAve77oP6HtG/IiEn7 +kpyUXRNvFsDE0czpJJBvL/aRFUJxuRK91jhjC68sA7NsKMGg5OXb5I5Jj36xAkEA +gIT7aFOYBFwGgQAQkWNKLvySgKbAZRTeLBacpHMuQdl1DfdntvAyqpAZ0lY0RKmW +G6aFKaqQfOXKCyWoUiVknQJAXrlgySFci/2ueKlIE1QqIiLSZ8V8OlpFLRnb1pzI +7U1yQXnTAEFYM560yJlzUpOb1V4cScGd365tiSMvxLOvTA== +-----END RSA PRIVATE KEY----- diff --git a/test/web/http_sigs/pub.key b/test/web/http_sigs/pub.key new file mode 100644 index 000000000..b3bbf6cb9 --- /dev/null +++ b/test/web/http_sigs/pub.key @@ -0,0 +1,6 @@ +-----BEGIN PUBLIC KEY----- +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDCFENGw33yGihy92pDjZQhl0C3 +6rPJj+CvfSC8+q28hxA161QFNUd13wuCTUcq0Qd2qsBe/2hFyc2DCJJg0h1L78+6 +Z4UMR7EOcpfdUE9Hf3m/hs+FUR45uBJeDK1HSFHD8bHKD6kv8FPGfJTotc+2xjJw +oYi+1hqp1fIekaxsyQIDAQAB +-----END PUBLIC KEY----- diff --git a/test/web/ostatus/activity_representer_test.exs b/test/web/ostatus/activity_representer_test.exs index 0a66b819a..3ee9034a7 100644 --- a/test/web/ostatus/activity_representer_test.exs +++ b/test/web/ostatus/activity_representer_test.exs @@ -121,6 +121,7 @@ test "an announce activity" do #{note_xml} + """ announce_xml = ActivityRepresenter.to_simple_form(announce, user) @@ -156,6 +157,7 @@ test "a like activity" do