Return salmon path for users, basic incoming salmon handling.
authorRoger Braun <roger@rogerbraun.net>
Mon, 24 Apr 2017 16:46:34 +0000 (18:46 +0200)
committerRoger Braun <roger@rogerbraun.net>
Mon, 24 Apr 2017 16:46:34 +0000 (18:46 +0200)
lib/pleroma/web/activity_pub/activity_pub.ex
lib/pleroma/web/ostatus/feed_representer.ex
lib/pleroma/web/ostatus/ostatus.ex
lib/pleroma/web/ostatus/ostatus_controller.ex
lib/pleroma/web/router.ex
lib/pleroma/web/twitter_api/twitter_api.ex
lib/pleroma/web/web_finger/web_finger.ex
test/web/ostatus/feed_representer_test.exs
test/web/ostatus/ostatus_test.exs [new file with mode: 0644]

index e9f0dcd3290bfb213c596c1cca2f4400610d2ace..7264123d8aadf4fbe796d23065bc4efbe24dfff2 100644 (file)
@@ -19,6 +19,48 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub 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.
index 14ac3ebf4786fccd6feef406820211a7fddb6cd8..2cc0da9baf554d7577a706fe9256a73a0c9deb40 100644 (file)
@@ -23,6 +23,7 @@ defmodule Pleroma.Web.OStatus.FeedRepresenter 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
index d21b9078f4bc81799c7aee86e648de56171ab177..4fd649c92093575e6ba83e6abf1979ae2d6be122 100644 (file)
@@ -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 @@ defmodule Pleroma.Web.OStatus 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
index 3c8d8c0f11e665c60366435620bb3573cb98d499..4174db786c688c69826ad964a6dfd8a2b51d1674 100644 (file)
@@ -25,7 +25,14 @@ defmodule Pleroma.Web.OStatus.OStatusController 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
index a4f13c879b6746eea9accef186c95bffcde96f62..c98eac688cb9bf1af20a0bb942966d7d2cc94cd0 100644 (file)
@@ -74,6 +74,7 @@ defmodule Pleroma.Web.Router 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
 
index 0f84cffbd8bd8459ea43fec6d677043adad036b3..9049b4efcc14d4ea1d1d0335f7ccafee469b7b1d 100644 (file)
@@ -28,11 +28,33 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI 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 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI 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
index eb540e92ac011e3bc629f68f10df047961967788..18459e8f0c4ffb50079215a8a0e9ce1998ec0178 100644 (file)
@@ -31,7 +31,8 @@ defmodule Pleroma.Web.WebFinger 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
index 9a02d8c16cae22ca3be501aa824fdf14ddc9416a..13cdeb79d357cdf4ee813c69bd875351e30b3b10 100644 (file)
@@ -27,6 +27,7 @@ defmodule Pleroma.Web.OStatus.FeedRepresenterTest do
       <title>#{user.nickname}'s timeline</title>
       <updated>#{most_recent_update}</updated>
       <link rel="hub" href="#{OStatus.pubsub_path(user)}" />
+      <link rel="salmon" href="#{OStatus.salmon_path(user)}" />
       <link rel="self" href="#{OStatus.feed_path(user)}" />
       <author>
         #{user_xml}
diff --git a/test/web/ostatus/ostatus_test.exs b/test/web/ostatus/ostatus_test.exs
new file mode 100644 (file)
index 0000000..8ee6054
--- /dev/null
@@ -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