From ab0114fbaabd28d1e1a6961f6bfbd683f3e7fbbc Mon Sep 17 00:00:00 2001 From: Roger Braun Date: Mon, 24 Apr 2017 18:46:34 +0200 Subject: [PATCH] Return salmon path for users, basic incoming salmon handling. --- lib/pleroma/web/activity_pub/activity_pub.ex | 42 ++++++ lib/pleroma/web/ostatus/feed_representer.ex | 1 + lib/pleroma/web/ostatus/ostatus.ex | 134 +++++++++++++++++- lib/pleroma/web/ostatus/ostatus_controller.ex | 11 +- lib/pleroma/web/router.ex | 1 + lib/pleroma/web/twitter_api/twitter_api.ex | 65 ++++----- lib/pleroma/web/web_finger/web_finger.ex | 3 +- test/web/ostatus/feed_representer_test.exs | 1 + test/web/ostatus/ostatus_test.exs | 53 +++++++ 9 files changed, 272 insertions(+), 39 deletions(-) create mode 100644 test/web/ostatus/ostatus_test.exs diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index e9f0dcd32..7264123d8 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -19,6 +19,48 @@ def insert(map) when is_map(map) do Repo.insert(%Activity{data: map}) end + def create(to, actor, context, object, additional \\ %{}, published \\ nil) do + published = published || make_date() + + activity = %{ + "type" => "Create", + "to" => to, + "actor" => actor.ap_id, + "object" => object, + "published" => published, + "context" => context + } + |> Map.merge(additional) + + with {:ok, activity} <- insert(activity) do + {:ok, activity} = add_conversation_id(activity) + + if actor.local do + Pleroma.Web.Websub.publish(Pleroma.Web.OStatus.feed_path(actor), actor, activity) + end + + {:ok, activity} + end + end + + defp add_conversation_id(activity) do + if is_integer(activity.data["statusnetConversationId"]) do + {:ok, activity} + else + data = activity.data + |> put_in(["object", "statusnetConversationId"], activity.id) + |> put_in(["statusnetConversationId"], activity.id) + + object = Object.get_by_ap_id(activity.data["object"]["id"]) + + changeset = Ecto.Changeset.change(object, data: data["object"]) + Repo.update(changeset) + + changeset = Ecto.Changeset.change(activity, data: data) + Repo.update(changeset) + end + end + def like(%User{ap_id: ap_id} = user, %Object{data: %{ "id" => id}} = object) do cond do # There's already a like here, so return the original activity. diff --git a/lib/pleroma/web/ostatus/feed_representer.ex b/lib/pleroma/web/ostatus/feed_representer.ex index 14ac3ebf4..2cc0da9ba 100644 --- a/lib/pleroma/web/ostatus/feed_representer.ex +++ b/lib/pleroma/web/ostatus/feed_representer.ex @@ -23,6 +23,7 @@ def to_simple_form(user, activities, users) do {:title, ['#{user.nickname}\'s timeline']}, {:updated, h.(most_recent_update)}, {:link, [rel: 'hub', href: h.(OStatus.pubsub_path(user))], []}, + {:link, [rel: 'salmon', href: h.(OStatus.salmon_path(user))], []}, {:link, [rel: 'self', href: h.(OStatus.feed_path(user))], []}, {:author, UserRepresenter.to_simple_form(user)}, ] ++ entries diff --git a/lib/pleroma/web/ostatus/ostatus.ex b/lib/pleroma/web/ostatus/ostatus.ex index d21b9078f..4fd649c92 100644 --- a/lib/pleroma/web/ostatus/ostatus.ex +++ b/lib/pleroma/web/ostatus/ostatus.ex @@ -1,5 +1,9 @@ defmodule Pleroma.Web.OStatus do - alias Pleroma.Web + import Ecto.Query + require Logger + + alias Pleroma.{Repo, User, Web} + alias Pleroma.Web.ActivityPub.ActivityPub def feed_path(user) do "#{user.ap_id}/feed.atom" @@ -9,6 +13,132 @@ def pubsub_path(user) do "#{Web.base_url}/push/hub/#{user.nickname}" end - def user_path(user) do + def salmon_path(user) do + "#{user.ap_id}/salmon" + end + + def handle_incoming(xml_string) do + {doc, _rest} = :xmerl_scan.string(to_charlist(xml_string)) + + {:xmlObj, :string, object_type } = :xmerl_xpath.string('string(/entry/activity:object-type[1])', doc) + + case object_type do + 'http://activitystrea.ms/schema/1.0/note' -> + handle_note(doc) + _ -> + Logger.error("Couldn't parse incoming document") + end + end + + # TODO + # Parse mention + # wire up replies + # Set correct context + # Set correct statusnet ids. + def handle_note(doc) do + content_html = string_from_xpath("/entry/content[1]", doc) + + [author] = :xmerl_xpath.string('/entry/author[1]', doc) + {:ok, actor} = find_or_make_user(author) + + context = ActivityPub.generate_context_id + + to = [ + "https://www.w3.org/ns/activitystreams#Public" + ] + + date = string_from_xpath("/entry/published", doc) + + object = %{ + "type" => "Note", + "to" => to, + "content" => content_html, + "published" => date, + "context" => context, + "actor" => actor.ap_id + } + + ActivityPub.create(to, actor, context, object, %{}, date) + end + + def find_or_make(author, doc) do + query = from user in User, + where: user.local == false and fragment("? @> ?", user.info, ^%{ostatus_uri: author}) + + user = Repo.one(query) + + if is_nil(user) do + make_user(doc) + else + {:ok, user} + end + end + + def find_or_make_user(author_doc) do + {:xmlObj, :string, uri } = :xmerl_xpath.string('string(/author[1]/uri)', author_doc) + + query = from user in User, + where: user.local == false and fragment("? @> ?", user.info, ^%{ostatus_uri: to_string(uri)}) + + user = Repo.one(query) + + if is_nil(user) do + make_user(author_doc) + else + {:ok, user} + end + end + + defp string_from_xpath(xpath, doc) do + {:xmlObj, :string, res} = :xmerl_xpath.string('string(#{xpath})', doc) + + res = res + |> to_string + |> String.trim + + if res == "", do: nil, else: res + end + + def make_user(author_doc) do + author = string_from_xpath("/author[1]/uri", author_doc) + name = string_from_xpath("/author[1]/name", author_doc) + preferredUsername = string_from_xpath("/author[1]/poco:preferredUsername", author_doc) + displayName = string_from_xpath("/author[1]/poco:displayName", author_doc) + avatar = make_avatar_object(author_doc) + + data = %{ + local: false, + name: preferredUsername || name, + nickname: displayName || name, + ap_id: author, + info: %{ + "ostatus_uri" => author, + "host" => URI.parse(author).host, + "system" => "ostatus" + }, + avatar: avatar + } + + Repo.insert(Ecto.Changeset.change(%User{}, data)) + end + + # TODO: Just takes the first one for now. + defp make_avatar_object(author_doc) do + href = string_from_xpath("/author[1]/link[@rel=\"avatar\"]/@href", author_doc) + type = string_from_xpath("/author[1]/link[@rel=\"avatar\"]/@type", author_doc) + + if href do + %{ + "type" => "Image", + "url" => + [%{ + "type" => "Link", + "mediaType" => type, + "href" => href + }] + } + else + nil + end end end diff --git a/lib/pleroma/web/ostatus/ostatus_controller.ex b/lib/pleroma/web/ostatus/ostatus_controller.ex index 3c8d8c0f1..4174db786 100644 --- a/lib/pleroma/web/ostatus/ostatus_controller.ex +++ b/lib/pleroma/web/ostatus/ostatus_controller.ex @@ -25,7 +25,14 @@ def feed(conn, %{"nickname" => nickname}) do |> send_resp(200, response) end - def temp(conn, params) do - IO.inspect(params) + def salmon_incoming(conn, params) do + {:ok, body, _conn} = read_body(conn) + magic_key = Pleroma.Web.Salmon.fetch_magic_key(body) + {:ok, doc} = Pleroma.Web.Salmon.decode_and_validate(magic_key, body) + + Pleroma.Web.OStatus.handle_incoming(doc) + + conn + |> send_resp(200, "") end end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index a4f13c879..c98eac688 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -74,6 +74,7 @@ def user_fetcher(username) do pipe_through :ostatus get "/users/:nickname/feed", OStatus.OStatusController, :feed + post "/users/:nickname/salmon", OStatus.OStatusController, :salmon_incoming post "/push/hub/:nickname", Websub.WebsubController, :websub_subscription_request end diff --git a/lib/pleroma/web/twitter_api/twitter_api.ex b/lib/pleroma/web/twitter_api/twitter_api.ex index 0f84cffbd..9049b4efc 100644 --- a/lib/pleroma/web/twitter_api/twitter_api.ex +++ b/lib/pleroma/web/twitter_api/twitter_api.ex @@ -28,11 +28,33 @@ def create_status(user = %User{}, data = %{}) do date = make_date() - activity = %{ - "type" => "Create", - "to" => to, - "actor" => user.ap_id, - "object" => %{ + # Wire up reply info. + [to, context, object, additional] = + with inReplyToId when not is_nil(inReplyToId) <- data["in_reply_to_status_id"], + inReplyTo <- Repo.get(Activity, inReplyToId), + context <- inReplyTo.data["context"] + do + to = to ++ [inReplyTo.data["actor"]] + + object = %{ + "type" => "Note", + "to" => to, + "content" => content_html, + "published" => date, + "context" => context, + "attachment" => attachments, + "actor" => user.ap_id, + "inReplyTo" => inReplyTo.data["object"]["id"], + "inReplyToStatusId" => inReplyToId, + "statusnetConversationId" => inReplyTo.data["statusnetConversationId"] + } + additional = %{ + "statusnetConversationId" => inReplyTo.data["statusnetConversationId"] + } + + [to, context, object, additional] + else _e -> + object = %{ "type" => "Note", "to" => to, "content" => content_html, @@ -40,36 +62,11 @@ def create_status(user = %User{}, data = %{}) do "context" => context, "attachment" => attachments, "actor" => user.ap_id - }, - "published" => date, - "context" => context - } - - # Wire up reply info. - activity = with inReplyToId when not is_nil(inReplyToId) <- data["in_reply_to_status_id"], - inReplyTo <- Repo.get(Activity, inReplyToId), - context <- inReplyTo.data["context"] - do - - to = activity["to"] ++ [inReplyTo.data["actor"]] - - activity - |> put_in(["to"], to) - |> put_in(["context"], context) - |> put_in(["object", "context"], context) - |> put_in(["object", "inReplyTo"], inReplyTo.data["object"]["id"]) - |> put_in(["object", "inReplyToStatusId"], inReplyToId) - |> put_in(["statusnetConversationId"], inReplyTo.data["statusnetConversationId"]) - |> put_in(["object", "statusnetConversationId"], inReplyTo.data["statusnetConversationId"]) - else _e -> - activity - end - - with {:ok, activity} <- ActivityPub.insert(activity) do - {:ok, activity} = add_conversation_id(activity) - Pleroma.Web.Websub.publish(Pleroma.Web.OStatus.feed_path(user), user, activity) - {:ok, activity} + } + [to, context, object, %{}] end + + ActivityPub.create(to, user, context, object, additional, data) end def fetch_friend_statuses(user, opts \\ %{}) do diff --git a/lib/pleroma/web/web_finger/web_finger.ex b/lib/pleroma/web/web_finger/web_finger.ex index eb540e92a..18459e8f0 100644 --- a/lib/pleroma/web/web_finger/web_finger.ex +++ b/lib/pleroma/web/web_finger/web_finger.ex @@ -31,7 +31,8 @@ def represent_user(user) do [ {:Subject, "acct:#{user.nickname}@#{Pleroma.Web.host}"}, {:Alias, user.ap_id}, - {:Link, %{rel: "http://schemas.google.com/g/2010#updates-from", type: "application/atom+xml", href: OStatus.feed_path(user)}} + {:Link, %{rel: "http://schemas.google.com/g/2010#updates-from", type: "application/atom+xml", href: OStatus.feed_path(user)}}, + {:Link, %{rel: "salmon", href: OStatus.salmon_path(user)}} ] } |> XmlBuilder.to_doc diff --git a/test/web/ostatus/feed_representer_test.exs b/test/web/ostatus/feed_representer_test.exs index 9a02d8c16..13cdeb79d 100644 --- a/test/web/ostatus/feed_representer_test.exs +++ b/test/web/ostatus/feed_representer_test.exs @@ -27,6 +27,7 @@ test "returns a feed of the last 20 items of the user" do #{user.nickname}'s timeline #{most_recent_update} + #{user_xml} diff --git a/test/web/ostatus/ostatus_test.exs b/test/web/ostatus/ostatus_test.exs new file mode 100644 index 000000000..8ee605494 --- /dev/null +++ b/test/web/ostatus/ostatus_test.exs @@ -0,0 +1,53 @@ +defmodule Pleroma.Web.OStatusTest do + use Pleroma.DataCase + alias Pleroma.Web.OStatus + + test "handle incoming notes" do + incoming = File.read!("test/fixtures/incoming_note_activity.xml") + {:ok, activity} = OStatus.handle_incoming(incoming) + + assert activity.data["type"] == "Create" + assert activity.data["object"]["type"] == "Note" + assert activity.data["published"] == "2017-04-23T14:51:03+00:00" + end + + describe "new remote user creation" do + test "make new user or find them based on an 'author' xml doc" do + incoming = File.read!("test/fixtures/user_name_only.xml") + {doc, _rest} = :xmerl_scan.string(to_charlist(incoming)) + + {:ok, user} = OStatus.find_or_make_user(doc) + + assert user.name == "lambda" + assert user.nickname == "lambda" + assert user.local == false + assert user.info["ostatus_uri"] == "http://gs.example.org:4040/index.php/user/1" + assert user.info["system"] == "ostatus" + assert user.ap_id == "http://gs.example.org:4040/index.php/user/1" + + {:ok, user_again} = OStatus.find_or_make_user(doc) + + assert user == user_again + end + + test "tries to use the information in poco fields" do + incoming = File.read!("test/fixtures/user_full.xml") + {doc, _rest} = :xmerl_scan.string(to_charlist(incoming)) + + {:ok, user} = OStatus.find_or_make_user(doc) + + assert user.name == "Constance Variable" + assert user.nickname == "lambadalambda" + assert user.local == false + assert user.info["ostatus_uri"] == "http://gs.example.org:4040/index.php/user/1" + assert user.info["system"] == "ostatus" + assert user.ap_id == "http://gs.example.org:4040/index.php/user/1" + + assert List.first(user.avatar["url"])["href"] == "http://gs.example.org:4040/theme/neo-gnu/default-avatar-profile.png" + + {:ok, user_again} = OStatus.find_or_make_user(doc) + + assert user == user_again + end + end +end