diff options
Diffstat (limited to 'lib')
32 files changed, 1124 insertions, 123 deletions
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 @@ defmodule Pleroma.User 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 @@ defmodule Pleroma.User 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 @@ defmodule Pleroma.User 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 @@ defmodule Pleroma.User 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 @@ defmodule Pleroma.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 @@ defmodule Pleroma.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 @@ defmodule Pleroma.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 @@ defmodule Pleroma.User 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 @@ defmodule Pleroma.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 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub 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 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do 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 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub 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 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do 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 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub 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 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub 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 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub 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 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub 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 @@ defmodule Pleroma.Web.ActivityPub.Utils 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 @@ defmodule Pleroma.Web.ActivityPub.Utils do "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 @@ defmodule Pleroma.Web.ActivityPub.Utils do "type" => "Follow", "actor" => follower_id, "to" => [followed_id], + "cc" => ["https://www.w3.org/ns/activitystreams#Public"], "object" => followed_id } @@ -177,6 +179,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do "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 @@ defmodule Pleroma.Web.ActivityPub.Utils 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 @@ defmodule Pleroma.Web.CommonAPI 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 @@ defmodule Pleroma.Web.CommonAPI.Utils 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 @@ defmodule Pleroma.Web.CommonAPI.Utils 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 @@ defmodule Pleroma.Web.Federator 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 @@ defmodule Pleroma.Web.Federator 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 @@ defmodule Pleroma.Web.Federator 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 @@ defmodule Pleroma.Web.Federator 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 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController 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 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController 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 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController 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 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController 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 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController 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 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController 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 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController 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 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do 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 @@ defmodule Pleroma.Web.MastodonAPI.MastodonSocket 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 @@ defmodule Pleroma.Web.MastodonAPI.AccountView 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 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do 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 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do 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 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do } 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 @@ defmodule Pleroma.Web.OStatus.ActivityRepresenter do 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 @@ defmodule Pleroma.Web.OStatus.ActivityRepresenter do _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 @@ defmodule Pleroma.Web.OStatus.ActivityRepresenter do 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 @@ defmodule Pleroma.Web.OStatus.ActivityRepresenter do 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 @@ defmodule Pleroma.Web.OStatus.ActivityRepresenter do 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 @@ defmodule Pleroma.Web.OStatus.NoteHandler 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 @@ defmodule Pleroma.Web.OStatus.NoteHandler 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 @@ defmodule Pleroma.Web.OStatus 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 @@ defmodule Pleroma.Web.OStatus 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 @@ defmodule Pleroma.Web.OStatus 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 @@ defmodule Pleroma.Web.OStatus 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 @@ defmodule Pleroma.Web.OStatus 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 @@ defmodule Pleroma.Web.OStatus.OStatusController 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 @@ defmodule Pleroma.Web.OStatus.OStatusController 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 @@ defmodule Pleroma.Web.OStatus.OStatusController 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 @@ defmodule Pleroma.Web.OStatus.UserRepresenter 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 @@ defmodule Pleroma.Web.OStatus.UserRepresenter 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 @@ defmodule Pleroma.Web.Router 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 @@ defmodule Pleroma.Web.Router 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 @@ defmodule Pleroma.Web.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 @@ defmodule Pleroma.Web.Salmon 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 @@ defmodule Pleroma.Web.Salmon 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 @@ defmodule Pleroma.Web.Streamer 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 @@ defmodule Pleroma.Web.Streamer 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 @@ defmodule Pleroma.Web.TwitterAPI.Representers.ActivityRepresenter do } 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 @@ defmodule Pleroma.Web.TwitterAPI.Representers.ActivityRepresenter do 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 @@ defmodule Pleroma.Web.TwitterAPI.Representers.ActivityRepresenter do 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 @@ defmodule Pleroma.Web.TwitterAPI.Representers.ActivityRepresenter do "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 @@ defmodule Pleroma.Web.TwitterAPI.Representers.ObjectRepresenter 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 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI 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 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI 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 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI 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 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI 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 @@ defmodule Pleroma.Web.TwitterAPI.Controller 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 @@ defmodule Pleroma.Web.TwitterAPI.Controller 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 @@ defmodule Pleroma.Web.TwitterAPI.Controller 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 @@ defmodule Pleroma.Web.TwitterAPI.Controller do 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 @@ defmodule Pleroma.Web.TwitterAPI.Controller 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 @@ defmodule Pleroma.Web.WebFinger 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 @@ defmodule Pleroma.Web.WebFinger 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 @@ defmodule Pleroma.Web.WebFinger 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 @@ defmodule Pleroma.Web.WebFinger 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 @@ defmodule Pleroma.Web.Websub 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 @@ defmodule Pleroma.Web.Websub 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 |