Merge branch 'develop' into dtluna/pleroma-refactor/1
authorRoger Braun <roger@rogerbraun.net>
Fri, 5 May 2017 09:46:59 +0000 (11:46 +0200)
committerRoger Braun <roger@rogerbraun.net>
Fri, 5 May 2017 09:46:59 +0000 (11:46 +0200)
63 files changed:
TODO.txt
config/config.exs
config/test.exs
lib/pleroma/activity.ex
lib/pleroma/application.ex
lib/pleroma/object.ex
lib/pleroma/user.ex
lib/pleroma/web/activity_pub/activity_pub.ex
lib/pleroma/web/federator/federator.ex [new file with mode: 0644]
lib/pleroma/web/ostatus/activity_representer.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/salmon/salmon.ex
lib/pleroma/web/twitter_api/representers/activity_representer.ex
lib/pleroma/web/twitter_api/representers/user_representer.ex
lib/pleroma/web/twitter_api/twitter_api.ex
lib/pleroma/web/twitter_api/twitter_api_controller.ex
lib/pleroma/web/web.ex
lib/pleroma/web/web_finger/web_finger.ex
lib/pleroma/web/websub/websub.ex
lib/pleroma/web/websub/websub_client_subscription.ex [new file with mode: 0644]
lib/pleroma/web/websub/websub_controller.ex
lib/pleroma/web/xml/xml.ex [new file with mode: 0644]
priv/repo/migrations/20170423154511_add_fields_to_users.exs [new file with mode: 0644]
priv/repo/migrations/20170426154155_create_websub_client_subscription.exs [new file with mode: 0644]
priv/repo/migrations/20170427054757_add_user_and_hub.exs [new file with mode: 0644]
priv/repo/migrations/20170501124823_add_id_contraints_to_activities_and_objects.exs [new file with mode: 0644]
priv/repo/migrations/20170501133231_add_id_contraints_to_activities_and_objects_part_two.exs [new file with mode: 0644]
priv/repo/migrations/20170502083023_add_local_field_to_activities.exs [new file with mode: 0644]
test/activity_test.exs
test/fixtures/23211.atom [new file with mode: 0644]
test/fixtures/incoming_note_activity.xml [new file with mode: 0644]
test/fixtures/incoming_note_activity_answer.xml [new file with mode: 0644]
test/fixtures/incoming_reply_mastodon.xml [new file with mode: 0644]
test/fixtures/incoming_websub_gnusocial_attachments.xml [new file with mode: 0644]
test/fixtures/lambadalambda.atom [new file with mode: 0644]
test/fixtures/ostatus_incoming_post.xml [new file with mode: 0644]
test/fixtures/ostatus_incoming_reply.xml [new file with mode: 0644]
test/fixtures/private_key.pem [new file with mode: 0644]
test/fixtures/salmon2.xml [new file with mode: 0644]
test/fixtures/share-gs.xml [new file with mode: 0644]
test/fixtures/share.xml [new file with mode: 0644]
test/fixtures/user_full.xml [new file with mode: 0644]
test/fixtures/user_name_only.xml [new file with mode: 0644]
test/fixtures/webfinger.xml [new file with mode: 0644]
test/support/builders/activity_builder.ex
test/support/factory.ex
test/user_test.exs
test/web/activity_pub/activity_pub_test.exs
test/web/ostatus/activity_representer_test.exs
test/web/ostatus/feed_representer_test.exs
test/web/ostatus/ostatus_controller_test.exs
test/web/ostatus/ostatus_test.exs [new file with mode: 0644]
test/web/salmon/salmon_test.exs
test/web/twitter_api/representers/activity_representer_test.exs
test/web/twitter_api/representers/user_representer_test.exs
test/web/twitter_api/twitter_api_controller_test.exs
test/web/twitter_api/twitter_api_test.exs
test/web/web_finger/web_finger_test.exs
test/web/websub/websub_controller_test.exs
test/web/websub/websub_test.exs

index dd85c52392f0b001ff946b65a2e101579aad6410..304e95e779afde9f05948e35ba8a1ba572019d31 100644 (file)
--- a/TODO.txt
+++ b/TODO.txt
@@ -1,5 +1,9 @@
-- Add cache for user fetching / representing. (mostly in TwitterAPI.activity_to_status)
-
 Unliking:
 
 - Add a proper undo activity, find out how to ignore those in twitter api.
+
+WEBSUB:
+
+- Add unsubscription
+- Add periodical renewal
+
index 3826dddff24bf89ceeb7626ad4fb3a0281a745b4..a5df31b5a50f9f64a89a94868375637fd9128f24 100644 (file)
@@ -30,7 +30,8 @@ config :mime, :types, %{
   "application/xrd+xml" => ["xrd+xml"]
 }
 
-config :pleroma, :websub_verifier, Pleroma.Web.Websub
+config :pleroma, :websub, Pleroma.Web.Websub
+config :pleroma, :ostatus, Pleroma.Web.OStatus
 
 # Import environment specific config. This must remain at the bottom
 # of this file so it overrides the configuration defined above.
index 5d91279a22ae32927b22760825d6d3e0b73d7e73..85b6ad26b4654834971515b0065f121a7e3f1d2c 100644 (file)
@@ -25,4 +25,5 @@ config :pleroma, Pleroma.Repo,
 # Reduce hash rounds for testing
 config :comeonin, :pbkdf2_rounds, 1
 
-config :pleroma, :websub_verifier, Pleroma.Web.WebsubMock
+config :pleroma, :websub, Pleroma.Web.WebsubMock
+config :pleroma, :ostatus, Pleroma.Web.OStatusMock
index 46568bb13eb288d6322b145db776e5427aa5e9c8..d77c8899749a7dd16e568829e3a77340392afa31 100644 (file)
@@ -5,6 +5,7 @@ defmodule Pleroma.Activity do
 
   schema "activities" do
     field :data, :map
+    field :local, :boolean, default: true
 
     timestamps()
   end
@@ -18,4 +19,9 @@ defmodule Pleroma.Activity do
     Repo.all(from activity in Activity,
       where: fragment("? @> ?", activity.data, ^%{object: %{id: ap_id}}))
   end
+
+  def get_create_activity_by_object_ap_id(ap_id) do
+    Repo.one(from activity in Activity,
+      where: fragment("? @> ?", activity.data, ^%{type: "Create", object: %{id: ap_id}}))
+  end
 end
index 86b6c0c1ea59ca953d2a6e18e248f165273dacaa..6267d0695b59ce9b5b59ed5941f09c7addf2fa82 100644 (file)
@@ -15,9 +15,9 @@ defmodule Pleroma.Application do
       # Start your own worker by calling: Pleroma.Worker.start_link(arg1, arg2, arg3)
       # worker(Pleroma.Worker, [arg1, arg2, arg3]),
       worker(Cachex, [:user_cache, [
-                         default_ttl: 5000,
+                         default_ttl: 25000,
                          ttl_interval: 1000,
-                         limit: 500
+                         limit: 2500
                        ]])
     ]
 
index f932034d7b99b2b9b8b0a4c90054fba43f71fc7f..949ccb0f66e172a6b4bece0983e55ef20fa52b8d 100644 (file)
@@ -13,4 +13,24 @@ defmodule Pleroma.Object do
     Repo.one(from object in Object,
       where: fragment("? @> ?", object.data, ^%{id: ap_id}))
   end
+
+  def get_cached_by_ap_id(ap_id) do
+    if Mix.env == :test do
+      get_by_ap_id(ap_id)
+    else
+      key = "object:#{ap_id}"
+      Cachex.get!(:user_cache, key, fallback: fn(_) ->
+        object = get_by_ap_id(ap_id)
+        if object do
+          {:commit, object}
+        else
+          {:ignore, object}
+        end
+      end)
+    end
+  end
+
+  def context_mapping(context) do
+    %Object{data: %{"id" => context}}
+  end
 end
index 65925caed0aa3eee377b8264bbaa277e269b4bf9..23be6276ebe6abce6bffa9567d4c5a7d6225b600 100644 (file)
@@ -1,8 +1,10 @@
 defmodule Pleroma.User do
   use Ecto.Schema
+
   import Ecto.{Changeset, Query}
   alias Pleroma.{Repo, User, Object, Web}
   alias Comeonin.Pbkdf2
+  alias Pleroma.Web.OStatus
 
   schema "users" do
     field :bio, :string
@@ -15,6 +17,8 @@ defmodule Pleroma.User do
     field :following, {:array, :string}, default: []
     field :ap_id, :string
     field :avatar, :map
+    field :local, :boolean, default: true
+    field :info, :map, default: %{}
 
     timestamps()
   end
@@ -118,6 +122,27 @@ defmodule Pleroma.User do
 
   def get_cached_by_nickname(nickname) do
     key = "nickname:#{nickname}"
-    Cachex.get!(:user_cache, key, fallback: fn(_) -> Repo.get_by(User, nickname: nickname) end)
+    Cachex.get!(:user_cache, key, fallback: fn(_) -> get_or_fetch_by_nickname(nickname) end)
+  end
+
+  def get_by_nickname(nickname) do
+    Repo.get_by(User, nickname: nickname)
+  end
+
+  def get_cached_user_info(user) do
+    key = "user_info:#{user.id}"
+    Cachex.get!(:user_cache, key, fallback: fn(_) -> user_info(user) 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
+        user
+      else _e -> nil
+      end
+    end
   end
 end
index 02255e0a427c7fae327a92da12e3b31e2cee9981..d7b490088da2902fe5cb9145c83cf746e72287b9 100644 (file)
@@ -3,7 +3,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
   alias Ecto.{Changeset, UUID}
   import Ecto.Query
 
-  def insert(map) when is_map(map) do
+  def insert(map, local \\ true) when is_map(map) do
     map = map
     |> Map.put_new_lazy("id", &generate_activity_id/0)
     |> Map.put_new_lazy("published", &make_date/0)
@@ -16,7 +16,29 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
       map
     end
 
-    Repo.insert(%Activity{data: map})
+    Repo.insert(%Activity{data: map, local: local})
+  end
+
+  def create(to, actor, context, object, additional \\ %{}, published \\ nil, local \\ true) do
+    published = published || make_date()
+
+    activity = %{
+      "type" => "Create",
+      "to" => to |> Enum.uniq,
+      "actor" => actor.ap_id,
+      "object" => object,
+      "published" => published,
+      "context" => context
+    }
+    |> Map.merge(additional)
+
+    with {:ok, activity} <- insert(activity, local) do
+      if actor.local do
+        Pleroma.Web.Federator.enqueue(:publish, activity)
+       end
+
+      {:ok, activity}
+    end
   end
 
   def like(%User{ap_id: ap_id} = user, %Object{data: %{"id" => id}} = object) do
@@ -33,7 +55,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
           "type" => "Like",
           "actor" => ap_id,
           "object" => id,
-          "to" => [User.ap_followers(user), object.data["actor"]]
+          "to" => [User.ap_followers(user), object.data["actor"]],
+          "context" => object.data["context"]
         }
 
         {:ok, activity} = insert(data)
@@ -49,6 +72,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
 
         update_object_in_activities(object)
 
+        if user.local do
+          Pleroma.Web.Federator.enqueue(:publish, activity)
+        end
+
         {:ok, activity, object}
     end
   end
@@ -99,7 +126,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
   end
 
   def generate_object_id do
-    generate_id("objects")
+    Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :object, Ecto.UUID.generate)
   end
 
   def generate_id(type) do
@@ -127,6 +154,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
     query = from activity in query,
       where: activity.id > ^since_id
 
+    query = if opts["local_only"] do
+      from activity in query, where: activity.local == true
+    else
+      query
+    end
+
     query = if opts["max_id"] do
       from activity in query, where: activity.id < ^opts["max_id"]
     else
@@ -143,15 +176,16 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
     Enum.reverse(Repo.all(query))
   end
 
-  def announce(%User{ap_id: ap_id} = user, %Object{data: %{"id" => id}} = object) do
+  def announce(%User{ap_id: ap_id} = user, %Object{data: %{"id" => id}} = object, local \\ true) do
     data = %{
       "type" => "Announce",
       "actor" => ap_id,
       "object" => id,
-      "to" => [User.ap_followers(user), object.data["actor"]]
+      "to" => [User.ap_followers(user), object.data["actor"]],
+      "context" => object.data["context"]
     }
 
-    {:ok, activity} = insert(data)
+    {:ok, activity} = insert(data, local)
 
     announcements = [ap_id | (object.data["announcements"] || [])] |> Enum.uniq
 
@@ -164,6 +198,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
 
     update_object_in_activities(object)
 
+    if user.local do
+      Pleroma.Web.Federator.enqueue(:publish, activity)
+    end
+
     {:ok, activity, object}
   end
 
diff --git a/lib/pleroma/web/federator/federator.ex b/lib/pleroma/web/federator/federator.ex
new file mode 100644 (file)
index 0000000..675e804
--- /dev/null
@@ -0,0 +1,38 @@
+defmodule Pleroma.Web.Federator do
+  alias Pleroma.User
+  alias Pleroma.Web.WebFinger
+  require Logger
+
+  @websub Application.get_env(:pleroma, :websub)
+
+  def handle(:publish, activity) do
+    Logger.debug("Running publish for #{activity.data["id"]}")
+    with actor when not is_nil(actor) <- User.get_cached_by_ap_id(activity.data["actor"]) do
+      Logger.debug("Sending #{activity.data["id"]} out via websub")
+      Pleroma.Web.Websub.publish(Pleroma.Web.OStatus.feed_path(actor), actor, activity)
+
+      {:ok, actor} = WebFinger.ensure_keys_present(actor)
+      Logger.debug("Sending #{activity.data["id"]} out via salmon")
+      Pleroma.Web.Salmon.publish(actor, activity)
+    end
+  end
+
+  def handle(:verify_websub, websub) do
+    Logger.debug("Running websub verification for #{websub.id} (#{websub.topic}, #{websub.callback})")
+    @websub.verify(websub)
+  end
+
+  def handle(type, payload) do
+    Logger.debug("Unknown task: #{type}")
+    {:error, "Don't know what do do with this"}
+  end
+
+  def enqueue(type, payload) do
+    # for now, just run immediately in a new process.
+    if Mix.env == :test do
+      handle(type, payload)
+    else
+      spawn(fn -> handle(type, payload) end)
+    end
+  end
+end
index d7ea6132103b7eb96bc49ed40c7d022354e8982f..88781626c889a20ddebd11953592bc3025f1c8bd 100644 (file)
@@ -1,5 +1,31 @@
 defmodule Pleroma.Web.OStatus.ActivityRepresenter do
-  def to_simple_form(%{data: %{"object" => %{"type" => "Note"}}} = activity, user) do
+  alias Pleroma.{Activity, User}
+  alias Pleroma.Web.OStatus.UserRepresenter
+  require Logger
+
+  defp get_in_reply_to(%{"object" => %{ "inReplyTo" => in_reply_to}}) do
+    [{:"thr:in-reply-to", [ref: to_charlist(in_reply_to)], []}]
+  end
+
+  defp get_in_reply_to(_), do: []
+
+  defp get_mentions(to) do
+    Enum.map(to, fn (id) ->
+      cond do
+        # Special handling for the AP/Ostatus public collections
+        "https://www.w3.org/ns/activitystreams#Public" == id ->
+          {:link, [rel: "mentioned", "ostatus:object-type": "http://activitystrea.ms/schema/1.0/collection", href: "http://activityschema.org/collection/public"], []}
+        # Ostatus doesn't handle follower collections, ignore these.
+        Regex.match?(~r/^#{Pleroma.Web.base_url}.+followers$/, id) ->
+          []
+        true ->
+          {:link, [rel: "mentioned", "ostatus:object-type": "http://activitystrea.ms/schema/1.0/person", href: id], []}
+      end
+    end)
+  end
+
+  def to_simple_form(activity, user, with_author \\ false)
+  def to_simple_form(%{data: %{"object" => %{"type" => "Note"}}} = activity, user, with_author) do
     h = fn(str) -> [to_charlist(str)] end
 
     updated_at = activity.updated_at
@@ -12,16 +38,97 @@ defmodule Pleroma.Web.OStatus.ActivityRepresenter do
       {:link, [rel: 'enclosure', href: to_charlist(url["href"]), type: to_charlist(url["mediaType"])], []}
     end)
 
+    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
+
     [
       {:"activity:object-type", ['http://activitystrea.ms/schema/1.0/note']},
       {:"activity:verb", ['http://activitystrea.ms/schema/1.0/post']},
-      {:id, h.(activity.data["object"]["id"])},
+      {:id, h.(activity.data["object"]["id"])}, # For notes, federate the object id.
       {:title, ['New note by #{user.nickname}']},
       {:content, [type: 'html'], h.(activity.data["object"]["content"])},
       {:published, h.(inserted_at)},
-      {:updated, h.(updated_at)}
-    ] ++ attachments
+      {:updated, h.(updated_at)},
+      {:"ostatus:conversation", [], h.(activity.data["context"])},
+      {:link, [href: h.(activity.data["context"]), rel: 'ostatus:conversation'], []},
+      {:link, [type: ['application/atom+xml'], href: h.(activity.data["object"]["id"]), rel: 'self'], []}
+    ] ++ attachments ++ in_reply_to ++ author ++ mentions
+  end
+
+  def to_simple_form(%{data: %{"type" => "Like"}} = activity, user, with_author) do
+    h = fn(str) -> [to_charlist(str)] end
+
+    updated_at = activity.updated_at
+    |> NaiveDateTime.to_iso8601
+    inserted_at = activity.inserted_at
+    |> NaiveDateTime.to_iso8601
+
+    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
+
+    [
+      {:"activity:verb", ['http://activitystrea.ms/schema/1.0/favorite']},
+      {:id, h.(activity.data["id"])},
+      {:title, ['New favorite by #{user.nickname}']},
+      {:content, [type: 'html'], ['#{user.nickname} favorited something']},
+      {:published, h.(inserted_at)},
+      {:updated, h.(updated_at)},
+      {:"activity:object", [
+        {:"activity:object-type", ['http://activitystrea.ms/schema/1.0/note']},
+        {:id, h.(activity.data["object"])}, # For notes, federate the object id.
+      ]},
+      {:"ostatus:conversation", [], h.(activity.data["context"])},
+      {:link, [href: h.(activity.data["context"]), rel: 'ostatus:conversation'], []},
+      {:link, [rel: 'self', type: ['application/atom+xml'], href: h.(activity.data["id"])], []},
+      {:"thr:in-reply-to", [ref: to_charlist(activity.data["object"])], []}
+    ] ++ author ++ mentions
+  end
+
+  def to_simple_form(%{data: %{"type" => "Announce"}} = activity, user, with_author) do
+    h = fn(str) -> [to_charlist(str)] end
+
+    updated_at = activity.updated_at
+    |> NaiveDateTime.to_iso8601
+    inserted_at = activity.inserted_at
+    |> NaiveDateTime.to_iso8601
+
+    in_reply_to = get_in_reply_to(activity.data)
+    author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: []
+
+    retweeted_activity = Activity.get_create_activity_by_object_ap_id(activity.data["object"])
+    retweeted_user = User.get_cached_by_ap_id(retweeted_activity.data["actor"])
+
+    retweeted_xml = to_simple_form(retweeted_activity, retweeted_user, true)
+
+    mentions = activity.data["to"] |> get_mentions
+    [
+      {:"activity:object-type", ['http://activitystrea.ms/schema/1.0/activity']},
+      {:"activity:verb", ['http://activitystrea.ms/schema/1.0/share']},
+      {:id, h.(activity.data["id"])},
+      {:title, ['#{user.nickname} repeated a notice']},
+      {:content, [type: 'html'], ['RT #{retweeted_activity.data["object"]["content"]}']},
+      {:published, h.(inserted_at)},
+      {:updated, h.(updated_at)},
+      {:"ostatus:conversation", [], h.(activity.data["context"])},
+      {:link, [href: h.(activity.data["context"]), rel: 'ostatus:conversation'], []},
+      {:link, [rel: 'self', type: ['application/atom+xml'], href: h.(activity.data["id"])], []},
+      {:"activity:object", retweeted_xml}
+    ] ++ mentions ++ author
+  end
+
+  def wrap_with_entry(simple_form) do
+    [{
+      :entry, [
+        xmlns: 'http://www.w3.org/2005/Atom',
+        "xmlns:thr": 'http://purl.org/syndication/thread/1.0',
+        "xmlns:activity": 'http://activitystrea.ms/spec/1.0/',
+        "xmlns:poco": 'http://portablecontacts.net/spec/1.0',
+        "xmlns:ostatus": 'http://ostatus.org/schema/1.0'
+      ], simple_form
+    }]
   end
 
-  def to_simple_form(_, _), do: nil
+  def to_simple_form(_, _, _), do: nil
 end
index 86c6f9d4f3fedc41c3254d395c41623aecd7d1be..6b67b8ddf4f15485440e2e6dfac7a190318f37f1 100644 (file)
@@ -17,14 +17,17 @@ defmodule Pleroma.Web.OStatus.FeedRepresenter do
     [{
       :feed, [
         xmlns: 'http://www.w3.org/2005/Atom',
+        "xmlns:thr": 'http://purl.org/syndication/thread/1.0',
         "xmlns:activity": 'http://activitystrea.ms/spec/1.0/',
-        "xmlns:poco": 'http://portablecontacts.net/spec/1.0'
+        "xmlns:poco": 'http://portablecontacts.net/spec/1.0',
+        "xmlns:ostatus": 'http://ostatus.org/schema/1.0'
       ], [
         {:id, h.(OStatus.feed_path(user))},
         {:title, ['#{user.nickname}\'s timeline']},
         {:updated, h.(most_recent_update)},
         {:link, [rel: 'hub', href: h.(OStatus.pubsub_path(user))], []},
-        {:link, [rel: 'self', href: h.(OStatus.feed_path(user))], []},
+        {:link, [rel: 'salmon', href: h.(OStatus.salmon_path(user))], []},
+        {:link, [rel: 'self', href: h.(OStatus.feed_path(user)), type: 'application/atom+xml'], []},
         {:author, UserRepresenter.to_simple_form(user)},
       ] ++ entries
     }]
index d21b9078f4bc81799c7aee86e648de56171ab177..2fab676631df3f16b7562bb7ac6db905a6d68671 100644 (file)
@@ -1,5 +1,11 @@
 defmodule Pleroma.Web.OStatus do
-  alias Pleroma.Web
+  import Ecto.Query
+  import Pleroma.Web.XML
+  require Logger
+
+  alias Pleroma.{Repo, User, Web, Object}
+  alias Pleroma.Web.ActivityPub.ActivityPub
+  alias Pleroma.Web.{WebFinger, Websub}
 
   def feed_path(user) do
     "#{user.ap_id}/feed.atom"
@@ -9,6 +15,199 @@ 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 = parse_document(xml_string)
+    entries = :xmerl_xpath.string('//entry', doc)
+
+    activities = Enum.map(entries, fn (entry) ->
+      {:xmlObj, :string, object_type } = :xmerl_xpath.string('string(/entry/activity:object-type[1])', entry)
+      {:xmlObj, :string, verb } = :xmerl_xpath.string('string(/entry/activity:verb[1])', entry)
+
+      case verb do
+        'http://activitystrea.ms/schema/1.0/share' ->
+          with {:ok, activity, retweeted_activity} <- handle_share(entry, doc), do: [activity, retweeted_activity]
+        _ ->
+          case object_type do
+            'http://activitystrea.ms/schema/1.0/note' ->
+              with {:ok, activity} <- handle_note(entry, doc), do: activity
+            'http://activitystrea.ms/schema/1.0/comment' ->
+              with {:ok, activity} <- handle_note(entry, doc), do: activity
+            _ ->
+              Logger.error("Couldn't parse incoming document")
+              nil
+          end
+      end
+    end)
+    {:ok, activities}
+  end
+
+  def make_share(entry, doc, retweeted_activity) do
+    with {:ok, actor} <- find_make_or_update_user(doc),
+         %Object{} = object <- Object.get_cached_by_ap_id(retweeted_activity.data["object"]["id"]),
+         {:ok, activity, object} = ActivityPub.announce(actor, object, false) do
+      {:ok, activity}
+    end
+  end
+
+  def handle_share(entry, doc) do
+    with [object] <- :xmerl_xpath.string('/entry/activity:object', entry),
+         {:ok, retweeted_activity} <-  handle_note(object, object),
+         {:ok, activity} <- make_share(entry, doc, retweeted_activity) do
+      {:ok, activity, retweeted_activity}
+    else
+      e -> {:error, e}
+    end
+  end
+
+  def get_attachments(entry) do
+    :xmerl_xpath.string('/entry/link[@rel="enclosure"]', entry)
+    |> Enum.map(fn (enclosure) ->
+      with href when not is_nil(href) <- string_from_xpath("/link/@href", enclosure),
+           type when not is_nil(type) <- string_from_xpath("/link/@type", enclosure) do
+        %{
+          "type" => "Attachment",
+          "url" => [%{
+                       "type" => "Link",
+                       "mediaType" => type,
+                       "href" => href
+                    }]
+        }
+      end
+    end)
+    |> Enum.filter(&(&1))
+  end
+
+  def handle_note(entry, doc \\ nil) do
+    content_html = string_from_xpath("//content[1]", entry)
+
+    [author] = :xmerl_xpath.string('//author[1]', doc)
+    {:ok, actor} = find_make_or_update_user(author)
+    inReplyTo = string_from_xpath("//thr:in-reply-to[1]/@ref", entry)
+
+    context = (string_from_xpath("//ostatus:conversation[1]", entry) || "") |> String.trim
+
+    attachments = get_attachments(entry)
+
+    context = with %{data: %{"context" => context}} <- Object.get_cached_by_ap_id(inReplyTo) do
+                context
+              else _e ->
+                if String.length(context) > 0 do
+                  context
+                else
+                  ActivityPub.generate_context_id
+                end
+              end
+
+    to = [
+      "https://www.w3.org/ns/activitystreams#Public"
+    ]
+
+    mentions = :xmerl_xpath.string('//link[@rel="mentioned" and @ostatus:object-type="http://activitystrea.ms/schema/1.0/person"]', entry)
+    |> Enum.map(fn(person) -> string_from_xpath("@href", person) end)
+
+    to = to ++ mentions
+
+    date = string_from_xpath("//published", entry)
+    id = string_from_xpath("//id", entry)
+
+    object = %{
+      "id" => id,
+      "type" => "Note",
+      "to" => to,
+      "content" => content_html,
+      "published" => date,
+      "context" => context,
+      "actor" => actor.ap_id,
+      "attachment" => attachments
+    }
+
+    object = if inReplyTo do
+      Map.put(object, "inReplyTo", inReplyTo)
+    else
+      object
+    end
+
+    # TODO: Bail out sooner and use transaction.
+    if Object.get_by_ap_id(id) do
+      {:error, "duplicate activity"}
+    else
+      ActivityPub.create(to, actor, context, object, %{}, date, false)
+    end
+  end
+
+  def find_make_or_update_user(doc) do
+    uri = string_from_xpath("//author/uri[1]", doc)
+    with {:ok, user} <- find_or_make_user(uri) do
+      avatar = make_avatar_object(doc)
+      if user.avatar != avatar do
+        change = Ecto.Changeset.change(user, %{avatar: avatar})
+        Repo.update(change)
+      else
+        {:ok, user}
+      end
+    end
+  end
+
+  def find_or_make_user(uri) do
+    query = from user in User,
+      where: user.local == false and fragment("? @> ?", user.info, ^%{uri: uri})
+
+    user = Repo.one(query)
+
+    if is_nil(user) do
+      make_user(uri)
+    else
+      {:ok, user}
+    end
+  end
+
+  def make_user(uri) do
+    with {:ok, info} <- gather_user_info(uri) do
+      data = %{
+        local: false,
+        name: info["name"],
+        nickname: info["nickname"] <> "@" <> info["host"],
+        ap_id: info["uri"],
+        info: info,
+        avatar: info["avatar"]
+      }
+      # TODO: Make remote user changeset
+      # SHould enforce fqn nickname
+      Repo.insert(Ecto.Changeset.change(%User{}, data))
+    end
+  end
+
+  # TODO: Just takes the first one for now.
+  def 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
+
+  def gather_user_info(username) do
+    with {:ok, webfinger_data} <- WebFinger.finger(username),
+         {:ok, feed_data} <- Websub.gather_feed_data(webfinger_data["topic"]) do
+      {:ok, Map.merge(webfinger_data, feed_data) |> Map.put("fqn", username)}
+    else e ->
+      Logger.debug("Couldn't gather info for #{username}")
+      {:error, e}
+    end
   end
 end
index ed7618d2b6836fc713467b90f90e513df8500c9a..e442562d58c878b9d526fb52b2fe64833ddd45ab 100644 (file)
@@ -2,10 +2,16 @@ defmodule Pleroma.Web.OStatus.OStatusController do
   use Pleroma.Web, :controller
 
   alias Pleroma.{User, Activity}
-  alias Pleroma.Web.OStatus.FeedRepresenter
+  alias Pleroma.Web.OStatus.{FeedRepresenter, ActivityRepresenter}
   alias Pleroma.Repo
+  alias Pleroma.Web.OStatus
   import Ecto.Query
 
+  def feed_redirect(conn, %{"nickname" => nickname}) do
+    user = User.get_cached_by_nickname(nickname)
+    redirect conn, external: OStatus.feed_path(user)
+  end
+
   def feed(conn, %{"nickname" => nickname}) do
     user = User.get_cached_by_nickname(nickname)
     query = from activity in Activity,
@@ -26,7 +32,29 @@ 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)
+    {:ok, 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
+
+  def object(conn, %{"uuid" => uuid}) do
+    id = o_status_url(conn, :object, uuid)
+    activity = Activity.get_create_activity_by_object_ap_id(id)
+    user = User.get_cached_by_ap_id(activity.data["actor"])
+
+    response = ActivityRepresenter.to_simple_form(activity, user, true)
+    |> ActivityRepresenter.wrap_with_entry
+    |> :xmerl.export_simple(:xmerl_xml)
+    |> to_string
+
+    conn
+    |> put_resp_content_type("application/atom+xml")
+    |> send_resp(200, response)
   end
 end
index 2c94d071fb0dd29c93b4c01fac3ba7e60279458d..327987d1d9c1995364e925e6071ada01ae9d20f7 100644 (file)
@@ -30,7 +30,7 @@ defmodule Pleroma.Web.Router do
     get "/statusnet/config", TwitterAPI.Controller, :config
 
     get "/statuses/public_timeline", TwitterAPI.Controller, :public_timeline
-    get "/statuses/public_and_external_timeline", TwitterAPI.Controller, :public_timeline
+    get "/statuses/public_and_external_timeline", TwitterAPI.Controller, :public_and_external_timeline
     get "/statuses/user_timeline", TwitterAPI.Controller, :user_timeline
 
     get "/statuses/show/:id", TwitterAPI.Controller, :fetch_status
@@ -73,8 +73,14 @@ defmodule Pleroma.Web.Router do
   scope "/", Pleroma.Web do
     pipe_through :ostatus
 
+    get "/objects/:uuid", OStatus.OStatusController, :object
+
     get "/users/:nickname/feed", OStatus.OStatusController, :feed
+    get "/users/:nickname", OStatus.OStatusController, :feed_redirect
+    post "/users/:nickname/salmon", OStatus.OStatusController, :salmon_incoming
     post "/push/hub/:nickname", Websub.WebsubController, :websub_subscription_request
+    get "/push/subscriptions/:id", Websub.WebsubController, :websub_subscription_confirmation
+    post "/push/subscriptions/:id", Websub.WebsubController, :websub_incoming
   end
 
   scope "/.well-known", Pleroma.Web do
@@ -92,5 +98,5 @@ end
 
 defmodule Fallback.RedirectController do
   use Pleroma.Web, :controller
-  def redirector(conn, _params), do: send_file(conn, 200, "priv/static/index.html")
+  def redirector(conn, _params), do: (if Mix.env != :test, do: send_file(conn, 200, "priv/static/index.html"))
 end
index f26daf824fe4ee46839067174593724cd8f53ec9..fe529c4c09ffa5450bd826a7951f3adda5bf851f 100644 (file)
@@ -1,8 +1,12 @@
 defmodule Pleroma.Web.Salmon do
   use Bitwise
+  alias Pleroma.Web.XML
+  alias Pleroma.Web.OStatus.ActivityRepresenter
+  alias Pleroma.User
+  require Logger
 
   def decode(salmon) do
-    {doc, _rest} = :xmerl_scan.string(to_charlist(salmon))
+    doc = XML.parse_document(salmon)
 
     {:xmlObj, :string, data} = :xmerl_xpath.string('string(//me:data[1])', doc)
     {:xmlObj, :string, sig} = :xmerl_xpath.string('string(//me:sig[1])', doc)
@@ -20,22 +24,12 @@ defmodule Pleroma.Web.Salmon do
   end
 
   def fetch_magic_key(salmon) do
-    [data, _, _, _, _] = decode(salmon)
-    {doc, _rest} = :xmerl_scan.string(to_charlist(data))
-    {:xmlObj, :string, uri} = :xmerl_xpath.string('string(//author[1]/uri)', doc)
-
-    uri = to_string(uri)
-    base = URI.parse(uri).host
-
-    # TODO: Find out if this endpoint is mandated by the standard.
-    {:ok, response} = HTTPoison.get(base <> "/.well-known/webfinger", ["Accept": "application/xrd+xml"], [params: [resource: uri]])
-
-    {doc, _rest} = :xmerl_scan.string(to_charlist(response.body))
-
-    {:xmlObj, :string, magickey} = :xmerl_xpath.string('string(//Link[@rel="magic-public-key"]/@href)', doc)
-    "data:application/magic-public-key," <> magickey = to_string(magickey)
-
-    magickey
+    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, magic_key}
+    end
   end
 
   def decode_and_validate(magickey, salmon) do
@@ -56,7 +50,7 @@ defmodule Pleroma.Web.Salmon do
     end
   end
 
-  defp decode_key("RSA." <> magickey) do
+  def decode_key("RSA." <> magickey) do
     make_integer = fn(bin) ->
       list = :erlang.binary_to_list(bin)
       Enum.reduce(list, 0, fn (el, acc) -> (acc <<< 8) ||| el end)
@@ -69,4 +63,91 @@ defmodule Pleroma.Web.Salmon do
 
     {:RSAPublicKey, modulus, exponent}
   end
+
+  def encode_key({:RSAPublicKey, modulus, exponent}) do
+    modulus_enc = :binary.encode_unsigned(modulus) |> Base.url_encode64
+    exponent_enc = :binary.encode_unsigned(exponent) |> Base.url_encode64
+
+    "RSA.#{modulus_enc}.#{exponent_enc}"
+  end
+
+  def generate_rsa_pem do
+    port = Port.open({:spawn, "openssl genrsa"}, [:binary])
+    {:ok, pem} = receive do
+      {^port, {:data, pem}} -> {:ok, pem}
+    end
+    Port.close(port)
+    if Regex.match?(~r/RSA PRIVATE KEY/, pem) do
+      {:ok, pem}
+    else
+      :error
+    end
+  end
+
+  def keys_from_pem(pem) do
+    [private_key_code] = :public_key.pem_decode(pem)
+    private_key = :public_key.pem_entry_decode(private_key_code)
+    {:RSAPrivateKey, _, modulus, exponent, _, _, _, _, _, _, _} = private_key
+    public_key = {:RSAPublicKey, modulus, exponent}
+    {:ok, private_key, public_key}
+  end
+
+  def encode(private_key, doc) do
+    type = "application/atom+xml"
+    encoding = "base64url"
+    alg = "RSA-SHA256"
+
+    signed_text = [doc, type, encoding, alg]
+    |> Enum.map(&Base.url_encode64/1)
+    |> Enum.join(".")
+
+    signature = :public_key.sign(signed_text, :sha256, private_key) |> to_string |> Base.url_encode64
+    doc_base64= doc |> Base.url_encode64
+
+    # Don't need proper xml building, these strings are safe to leave unescaped
+    salmon = """
+    <?xml version="1.0" encoding="UTF-8"?>
+    <me:env xmlns:me="http://salmon-protocol.org/ns/magic-env">
+      <me:data type="application/atom+xml">#{doc_base64}</me:data>
+      <me:encoding>#{encoding}</me:encoding>
+      <me:alg>#{alg}</me:alg>
+      <me:sig>#{signature}</me:sig>
+    </me:env>
+    """
+
+    {:ok, salmon}
+  end
+
+  def remote_users(%{data: %{"to" => to}}) do
+    to
+    |> Enum.map(fn(id) -> User.get_cached_by_ap_id(id) end)
+    |> Enum.filter(fn(user) -> user && !user.local end)
+  end
+
+  defp send_to_user(%{info: %{"salmon" => salmon}}, feed, poster) do
+    poster.(salmon, feed, [{"Content-Type", "application/magic-envelope+xml"}])
+  end
+
+  defp send_to_user(_,_,_), do: nil
+
+  def publish(user, activity, poster \\ &HTTPoison.post/3)
+  def publish(%{info: %{"keys" => keys}} = user, activity, poster) do
+    feed = ActivityRepresenter.to_simple_form(activity, user, true)
+    |> ActivityRepresenter.wrap_with_entry
+    |> :xmerl.export_simple(:xmerl_xml)
+    |> to_string
+
+    if feed do
+      {:ok, private, _} = keys_from_pem(keys)
+      {:ok, feed} = encode(private, feed)
+
+      remote_users(activity)
+      |> Enum.each(fn(remote_user) ->
+        Logger.debug("sending salmon to #{remote_user.ap_id}")
+        send_to_user(remote_user, feed, poster)
+      end)
+    end
+  end
+
+  def publish(%{id: id}, _, _), do: Logger.debug("Keys missing for user #{id}")
 end
index b5857282930a1e9e9226e9b80a6c7ef2d6162ea1..4d7ea0c5cfb21187c542df55ae6dec6376fa067f 100644 (file)
@@ -3,6 +3,7 @@ defmodule Pleroma.Web.TwitterAPI.Representers.ActivityRepresenter do
   alias Pleroma.Web.TwitterAPI.Representers.{UserRepresenter, ObjectRepresenter}
   alias Pleroma.{Activity, User}
   alias Calendar.Strftime
+  alias Pleroma.Web.TwitterAPI.TwitterAPI
 
   defp user_by_ap_id(user_list, ap_id) do
     Enum.find(user_list, fn (%{ap_id: user_id}) -> ap_id == user_id end)
@@ -81,6 +82,12 @@ defmodule Pleroma.Web.TwitterAPI.Representers.ActivityRepresenter do
     |> Enum.filter(&(&1))
     |> Enum.map(fn (user) -> UserRepresenter.to_map(user, opts) end)
 
+
+    conversation_id = with context when not is_nil(context) <- activity.data["context"] do
+      TwitterAPI.context_to_conversation_id(context)
+    else _e -> nil
+    end
+
     %{
       "id" => activity.id,
       "user" => UserRepresenter.to_map(user, opts),
@@ -91,7 +98,7 @@ defmodule Pleroma.Web.TwitterAPI.Representers.ActivityRepresenter do
       "is_post_verb" => true,
       "created_at" => created_at,
       "in_reply_to_status_id" => object["inReplyToStatusId"],
-      "statusnet_conversation_id" => object["statusnetConversationId"],
+      "statusnet_conversation_id" => conversation_id,
       "attachments" => (object["attachment"] || []) |> ObjectRepresenter.enum_to_list(opts),
       "attentions" => attentions,
       "fave_num" => like_count,
index ab7d6d353a7476357703f18631f53d3418ef043a..4930774138d8861f287eb48ede8a737328144001 100644 (file)
@@ -11,7 +11,7 @@ defmodule Pleroma.Web.TwitterAPI.Representers.UserRepresenter do
       false
     end
 
-    user_info = User.user_info(user)
+    user_info = User.get_cached_user_info(user)
 
     map = %{
       "id" => user.id,
@@ -28,7 +28,8 @@ defmodule Pleroma.Web.TwitterAPI.Representers.UserRepresenter do
       "profile_image_url_https" => image,
       "profile_image_url_profile_size" => image,
       "profile_image_url_original" => image,
-      "rights" => %{}
+      "rights" => %{},
+      "statusnet_profile_url" => user.ap_id
     }
 
     map
index 649936b76dbe10f1ce50a1ecb26b0f777cf45228..7656d4d33bdefcb10c76690b7c972b2329743968 100644 (file)
@@ -1,38 +1,81 @@
 defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
-  alias Ecto.Changeset
   alias Pleroma.{User, Activity, Repo, Object}
-  alias Pleroma.Web.{ActivityPub.ActivityPub, Websub, OStatus}
+  alias Pleroma.Web.ActivityPub.ActivityPub
   alias Pleroma.Web.TwitterAPI.Representers.{ActivityRepresenter, UserRepresenter}
 
   import Ecto.Query
 
-  def create_status(%User{} = user, %{} = data) do
-    attachments = Enum.map(data["media_ids"] || [], fn (media_id) ->
+  def to_for_user_and_mentions(user, mentions) do
+    default_to = [
+      User.ap_followers(user),
+      "https://www.w3.org/ns/activitystreams#Public"
+    ]
+
+    default_to ++ Enum.map(mentions, fn ({_, %{ap_id: ap_id}}) -> ap_id end)
+  end
+
+  def format_input(text, mentions) do
+    HtmlSanitizeEx.strip_tags(text)
+    |> String.replace("\n", "<br>")
+    |> add_user_links(mentions)
+  end
+
+  def attachments_from_ids(ids) do
+    Enum.map(ids || [], fn (media_id) ->
       Repo.get(Object, media_id).data
     end)
+  end
 
-    context = ActivityPub.generate_context_id
+  def get_replied_to_activity(id) when not is_nil(id) do
+    Repo.get(Activity, id)
+  end
 
-    content = data["status"] |> HtmlSanitizeEx.strip_tags |> String.replace("\n", "<br>")
+  def get_replied_to_activity(_), do: nil
 
-    mentions = parse_mentions(content)
+  def add_attachments(text, attachments) do
+    attachment_text = Enum.map(attachments, fn
+      (%{"url" => [%{"href" => href} | _]}) ->
+        "<a href='#{href}'>#{href}</a>"
+      _ -> ""
+    end)
+    Enum.join([text | attachment_text], "<br>")
+    end
 
-    default_to = [
-      User.ap_followers(user),
-      "https://www.w3.org/ns/activitystreams#Public"
-    ]
+  def create_status(user = %User{}, data = %{"status" => status}) do
+    attachments = attachments_from_ids(data["media_ids"])
+    context = ActivityPub.generate_context_id
+    mentions = parse_mentions(status)
+    content_html = status
+    |> format_input(mentions)
+    |> add_attachments(attachments)
 
-    to = default_to ++ Enum.map(mentions, fn ({_, %{ap_id: ap_id}}) -> ap_id end)
+    to = to_for_user_and_mentions(user, mentions)
+    date = make_date()
 
-    content_html = add_user_links(content, mentions)
+    inReplyTo = get_replied_to_activity(data["in_reply_to_status_id"])
 
-    date = make_date()
+    # Wire up reply info.
+    [to, context, object, additional] =
+      if inReplyTo do
+      context = inReplyTo.data["context"]
+      to = to ++ [inReplyTo.data["actor"]]
 
-    activity = %{
-      "type" => "Create",
-      "to" => to,
-      "actor" => user.ap_id,
-      "object" => %{
+      object = %{
+        "type" => "Note",
+        "to" => to,
+        "content" => content_html,
+        "published" => date,
+        "context" => context,
+        "attachment" => attachments,
+        "actor" => user.ap_id,
+        "inReplyTo" => inReplyTo.data["object"]["id"],
+        "inReplyToStatusId" => inReplyTo.id,
+      }
+      additional = %{}
+
+      [to, context, object, additional]
+      else
+      object = %{
         "type" => "Note",
         "to" => to,
         "content" => content_html,
@@ -40,65 +83,41 @@ 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)
-      Websub.publish(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
-    activities = ActivityPub.fetch_activities([user.ap_id | user.following], opts)
-    activities_to_statuses(activities, %{for: user})
+    ActivityPub.fetch_activities([user.ap_id | user.following], opts)
+    |> activities_to_statuses(%{for: user})
   end
 
   def fetch_public_statuses(user, opts \\ %{}) do
-    activities = ActivityPub.fetch_public_activities(opts)
-    activities_to_statuses(activities, %{for: user})
+    opts = Map.put(opts, "local_only", true)
+    ActivityPub.fetch_public_activities(opts)
+    |> activities_to_statuses(%{for: user})
+  end
+
+  def fetch_public_and_external_statuses(user, opts \\ %{}) do
+    ActivityPub.fetch_public_activities(opts)
+    |> activities_to_statuses(%{for: user})
   end
 
   def fetch_user_statuses(user, opts \\ %{}) do
-    activities = ActivityPub.fetch_activities([], opts)
-    activities_to_statuses(activities, %{for: user})
+    ActivityPub.fetch_activities([], opts)
+    |> activities_to_statuses(%{for: user})
   end
 
   def fetch_mentions(user, opts \\ %{}) do
-    activities = ActivityPub.fetch_activities([user.ap_id], opts)
-    activities_to_statuses(activities, %{for: user})
+    ActivityPub.fetch_activities([user.ap_id], opts)
+    |> activities_to_statuses(%{for: user})
   end
 
   def fetch_conversation(user, id) do
-    query = from activity in Activity,
-      where: fragment("? @> ?", activity.data, ^%{statusnetConversationId: id}),
-      limit: 1
-
-    with %Activity{} = activity <- Repo.one(query),
-         context <- activity.data["context"],
+    with context when is_binary(context) <- conversation_id_to_context(id),
          activities <- ActivityPub.fetch_activities_for_context(context),
          statuses <- activities |> activities_to_statuses(%{for: user})
     do
@@ -116,26 +135,26 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
   end
 
   def follow(%User{} = follower, params) do
-    with {:ok, %User{} = followed} <- get_user(params),
-         {:ok, follower} <- User.follow(follower, followed),
-         {:ok, activity} <- ActivityPub.insert(%{
-          "type" => "Follow",
-          "actor" => follower.ap_id,
-          "object" => followed.ap_id,
-          "published" => make_date()
-        })
+    with { :ok, %User{} = followed } <- get_user(params),
+         { :ok, follower } <- User.follow(follower, followed),
+         { :ok, activity } <- ActivityPub.insert(%{
+           "type" => "Follow",
+           "actor" => follower.ap_id,
+           "object" => followed.ap_id,
+           "published" => make_date()
+         })
     do
-      {:ok, follower, followed, activity}
+      { :ok, follower, followed, activity }
     else
       err -> err
     end
   end
 
   def unfollow(%User{} = follower, params) do
-    with {:ok, %User{} = unfollowed} <- get_user(params),
-         {:ok, follower} <- User.unfollow(follower, unfollowed)
+    with { :ok, %User{} = unfollowed } <- get_user(params),
+         { :ok, follower } <- User.unfollow(follower, unfollowed)
     do
-      {:ok, follower, unfollowed}
+      { :ok, follower, unfollowed}
     else
       err -> err
     end
@@ -207,7 +226,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
           media_id_string: "#{object.id}}",
           media_url: href,
           size: 0
-       } |> Poison.encode!
+        } |> Poison.encode!
     end
   end
 
@@ -215,36 +234,15 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
     # Modified from https://www.w3.org/TR/html5/forms.html#valid-e-mail-address
     regex = ~r/@[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@?[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*/
 
-    regex
-    |> Regex.scan(text)
+    Regex.scan(regex, text)
     |> List.flatten
     |> Enum.uniq
-    |> Enum.map(fn ("@" <> match = full_match) ->
-      {full_match, User.get_cached_by_nickname(match)} end)
+    |> Enum.map(fn ("@" <> match = full_match) -> {full_match, User.get_cached_by_nickname(match)} end)
     |> Enum.filter(fn ({_match, user}) -> user end)
   end
 
   def add_user_links(text, mentions) do
-    Enum.reduce(mentions, text, fn ({match, %User{ap_id: ap_id}}, text) ->
-      String.replace(text, match, "<a href='#{ap_id}'>#{match}</a>") 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 = Changeset.change(object, data: data["object"])
-      Repo.update(changeset)
-
-      changeset = Changeset.change(activity, data: data)
-      Repo.update(changeset)
-    end
+    Enum.reduce(mentions, text, fn ({match, %User{ap_id: ap_id}}, text) -> String.replace(text, match, "<a href='#{ap_id}'>#{match}</a>") end)
   end
 
   def register_user(params) do
@@ -255,7 +253,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
       email: params["email"],
       password: params["password"],
       password_confirmation: params["confirm"]
-   }
+    }
 
     changeset = User.register_changeset(%User{}, params)
 
@@ -263,21 +261,22 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
       {:ok, UserRepresenter.to_map(user)}
     else
       {:error, changeset} ->
-        errors = Poison.encode!(Changeset.traverse_errors(changeset, fn {msg, _opts} -> msg end))
-        {:error, %{error: errors}}
+        errors = Ecto.Changeset.traverse_errors(changeset, fn {msg, _opts} -> msg end)
+      |> Poison.encode!
+      {:error, %{error: errors}}
     end
   end
 
   def get_user(user \\ nil, params) do
     case params do
-      %{"user_id" => user_id} ->
+      %{ "user_id" => user_id } ->
         case target = Repo.get(User, user_id) do
           nil ->
             {:error, "No user with such user_id"}
           _ ->
             {:ok, target}
         end
-      %{"screen_name" => nickname} ->
+      %{ "screen_name" => nickname } ->
         case target = Repo.get_by(User, nickname: nickname) do
           nil ->
             {:error, "No user with such screen_name"}
@@ -305,8 +304,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
     user = User.get_cached_by_ap_id(actor)
     [liked_activity] = Activity.all_by_object_ap_id(activity.data["object"])
 
-    ActivityRepresenter.to_map(activity,
-      Map.merge(opts, %{user: user, liked_activity: liked_activity}))
+    ActivityRepresenter.to_map(activity, Map.merge(opts, %{user: user, liked_activity: liked_activity}))
   end
 
   # For announces, fetch the announced activity and the user.
@@ -316,8 +314,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
     [announced_activity] = Activity.all_by_object_ap_id(activity.data["object"])
     announced_actor = User.get_cached_by_ap_id(announced_activity.data["actor"])
 
-    ActivityRepresenter.to_map(activity,
-      Map.merge(opts, %{users: [user, announced_actor], announced_activity: announced_activity}))
+    ActivityRepresenter.to_map(activity, Map.merge(opts, %{users: [user, announced_actor], announced_activity: announced_activity}))
   end
 
   defp activity_to_status(activity, opts) do
@@ -327,7 +324,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
     mentioned_users = Enum.map(activity.data["to"] || [], fn (ap_id) ->
       User.get_cached_by_ap_id(ap_id)
     end)
-    mentioned_users = mentioned_users |> Enum.filter(&(&1))
+    |> Enum.filter(&(&1))
 
     ActivityRepresenter.to_map(activity, Map.merge(opts, %{user: user, mentioned: mentioned_users}))
   end
@@ -335,4 +332,22 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
   defp make_date do
     DateTime.utc_now() |> DateTime.to_iso8601
   end
+
+  def context_to_conversation_id(context) do
+    with %Object{id: id} <- Object.get_cached_by_ap_id(context) do
+      id
+    else _e ->
+      changeset = Object.context_mapping(context)
+      {:ok, %{id: id}} = Repo.insert(changeset)
+      id
+    end
+  end
+
+  def conversation_id_to_context(id) do
+    with %Object{data: %{"id" => context}} <- Repo.get(Object, id) do
+      context
+    else _e ->
+      {:error, "No such conversation"}
+    end
+  end
 end
index bbfc04a6aa9657383de184ac948d737c8cff128a..96a5f2151dd1b913f56064cfcc505519c1f0b629 100644 (file)
@@ -42,6 +42,14 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
     end
   end
 
+  def public_and_external_timeline(%{assigns: %{user: user}} = conn, params) do
+    statuses = TwitterAPI.fetch_public_and_external_statuses(user, params)
+    {:ok, json} = Poison.encode(statuses)
+
+    conn
+    |> json_reply(200, json)
+  end
+
   def public_timeline(%{assigns: %{user: user}} = conn, params) do
     statuses = TwitterAPI.fetch_public_statuses(user, params)
     {:ok, json} = Poison.encode(statuses)
index 19b1ff848590b3a6924d1cce9340d64d8141cd93..2c343c2d7c6395bfc9ca8f831e088bfbf560cb15 100644 (file)
@@ -58,28 +58,7 @@ defmodule Pleroma.Web do
     apply(__MODULE__, which, [])
   end
 
-  def host do
-    settings = Application.get_env(:pleroma, Pleroma.Web.Endpoint)
-    settings
-    |> Keyword.fetch!(:url)
-    |> Keyword.fetch!(:host)
-  end
-
   def base_url do
-    settings = Application.get_env(:pleroma, Pleroma.Web.Endpoint)
-
-    host = host()
-
-    protocol = settings |> Keyword.fetch!(:protocol)
-
-    port_fragment = with {:ok, protocol_info} <- settings
-                         |> Keyword.fetch(String.to_atom(protocol)),
-                         {:ok, port} <- protocol_info |> Keyword.fetch(:port)
-    do
-      ":#{port}"
-    else _e ->
-      ""
-    end
-    "#{protocol}://#{host}#{port_fragment}"
+    Pleroma.Web.Endpoint.url
   end
 end
index 3d6ca4e05c64c2d46dc32c2b7de4f44bff265e8a..1eb26a89fd19d5b355ac996b4407afee41705b50 100644 (file)
@@ -1,6 +1,9 @@
 defmodule Pleroma.Web.WebFinger do
-  alias Pleroma.{User, XmlBuilder}
-  alias Pleroma.{Web, Web.OStatus}
+
+  alias Pleroma.{Repo, User, XmlBuilder}
+  alias Pleroma.Web
+  alias Pleroma.Web.{XML, Salmon, OStatus}
+  require Logger
 
   def host_meta do
     base_url  = Web.base_url
@@ -14,25 +17,94 @@ defmodule Pleroma.Web.WebFinger do
   end
 
   def webfinger(resource) do
-    host = Web.host
-    regex = ~r/acct:(?<username>\w+)@#{host}/
-    case Regex.named_captures(regex, resource) do
-      %{"username" => username} ->
-        user = User.get_cached_by_nickname(username)
+    host = Pleroma.Web.Endpoint.host
+    regex = ~r/(acct:)?(?<username>\w+)@#{host}/
+    with %{"username" => username} <- Regex.named_captures(regex, resource) do
+      user = User.get_by_nickname(username)
+      {:ok, represent_user(user)}
+    else _e ->
+      with user when not is_nil(user) <- User.get_cached_by_ap_id(resource) do
         {:ok, represent_user(user)}
-      _ -> nil
+      else _e ->
+        {:error, "Couldn't find user"}
+      end
     end
   end
 
   def represent_user(user) do
+    {:ok, user} = ensure_keys_present(user)
+    {:ok, _private, public} = Salmon.keys_from_pem(user.info["keys"])
+    magic_key = Salmon.encode_key(public)
     {
       :XRD, %{xmlns: "http://docs.oasis-open.org/ns/xri/xrd-1.0"},
       [
-        {:Subject, "acct:#{user.nickname}@#{Web.host}"},
+        {:Subject, "acct:#{user.nickname}@#{Pleroma.Web.Endpoint.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: "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}"}}
       ]
     }
     |> XmlBuilder.to_doc
   end
+
+  # This seems a better fit in Salmon
+  def ensure_keys_present(user) do
+    info = user.info || %{}
+    if info["keys"] do
+      {:ok, user}
+    else
+      {:ok, pem} = Salmon.generate_rsa_pem
+      info = Map.put(info, "keys", pem)
+      Repo.update(Ecto.Changeset.change(user, info: info))
+    end
+  end
+
+  # FIXME: Make this call the host-meta to find the actual address.
+  defp webfinger_address(domain) do
+    "//#{domain}/.well-known/webfinger"
+  end
+
+  defp webfinger_from_xml(doc) do
+    magic_key = XML.string_from_xpath(~s{//Link[@rel="magic-public-key"]/@href}, doc)
+    "data:application/magic-public-key," <> magic_key = magic_key
+    topic = XML.string_from_xpath(~s{//Link[@rel="http://schemas.google.com/g/2010#updates-from"]/@href}, doc)
+    subject = XML.string_from_xpath("//Subject", doc)
+    salmon = XML.string_from_xpath(~s{//Link[@rel="salmon"]/@href}, doc)
+    data = %{
+      "magic_key" => magic_key,
+      "topic" => topic,
+      "subject" => subject,
+      "salmon" => salmon
+    }
+    {:ok, data}
+  end
+
+  def finger(account, getter \\ &HTTPoison.get/3) do
+    domain = with [_name, domain] <- String.split(account, "@") do
+               domain
+             else _e ->
+               URI.parse(account).host
+             end
+    address = webfinger_address(domain)
+
+    # try https first
+    response = with {:ok, result} <- getter.("https:" <> address, ["Accept": "application/xrd+xml"], [params: [resource: account]]) do
+                 {:ok, result}
+               else _ ->
+                 getter.("http:" <> address, ["Accept": "application/xrd+xml"], [params: [resource: account], follow_redirect: true])
+               end
+
+    with {:ok, %{status_code: status_code, body: body}} when status_code in 200..299 <- response,
+         doc <- XML.parse_document(body),
+         {:ok, data} <- webfinger_from_xml(doc) do
+      {:ok, data}
+    else
+      e ->
+        Logger.debug("Couldn't finger #{account}.")
+        Logger.debug(inspect(e))
+        {:error, e}
+    end
+  end
 end
index ba699db2462c5b31cb9a5df6d1c82cef77b0170b..afbe944c57996e0b08670c7598c7161b338fa1f3 100644 (file)
@@ -1,9 +1,11 @@
 defmodule Pleroma.Web.Websub do
   alias Ecto.Changeset
   alias Pleroma.Repo
-  alias Pleroma.Web.Websub.WebsubServerSubscription
+  alias Pleroma.Web.Websub.{WebsubServerSubscription, WebsubClientSubscription}
   alias Pleroma.Web.OStatus.FeedRepresenter
-  alias Pleroma.Web.OStatus
+  alias Pleroma.Web.{XML, Endpoint, OStatus}
+  alias Pleroma.Web.Router.Helpers
+  require Logger
 
   import Ecto.Query
 
@@ -44,8 +46,10 @@ defmodule Pleroma.Web.Websub do
       response = user
       |> FeedRepresenter.to_simple_form([activity], [user])
       |> :xmerl.export_simple(:xmerl_xml)
+      |> to_string
 
-      signature = Base.encode16(:crypto.hmac(:sha, sub.secret, response))
+      signature = sign(sub.secret || "", response)
+      Logger.debug("Pushing to #{sub.callback}")
 
       HTTPoison.post(sub.callback, response, [
             {"Content-Type", "application/atom+xml"},
@@ -54,6 +58,10 @@ defmodule Pleroma.Web.Websub do
     end)
   end
 
+  def sign(secret, doc) do
+    :crypto.hmac(:sha, secret, to_string(doc)) |> Base.encode16 |> String.downcase
+  end
+
   def incoming_subscription_request(user, %{"hub.mode" => "subscribe"} = params) do
     with {:ok, topic} <- valid_topic(params, user),
          {:ok, lease_time} <- lease_time(params),
@@ -75,11 +83,13 @@ defmodule Pleroma.Web.Websub do
                                           NaiveDateTime.add(websub.updated_at, lease_time)})
       websub = Repo.update!(change)
 
-      # Just spawn that for now, maybe pool later.
-      spawn(fn -> @websub_verifier.verify(websub) end)
+      Pleroma.Web.Federator.enqueue(:verify_websub, websub)
 
       {:ok, websub}
     else {:error, reason} ->
+      Logger.debug("Couldn't create subscription.")
+      Logger.debug(inspect(reason))
+
       {:error, reason}
     end
   end
@@ -89,6 +99,11 @@ defmodule Pleroma.Web.Websub do
       %WebsubServerSubscription{}
   end
 
+  # Temp hack for mastodon.
+  defp lease_time(%{"hub.lease_seconds" => ""}) do
+    {:ok, 60 * 60 * 24 * 3} # three days
+  end
+
   defp lease_time(%{"hub.lease_seconds" => lease_seconds}) do
     {:ok, String.to_integer(lease_seconds)}
   end
@@ -99,9 +114,92 @@ defmodule Pleroma.Web.Websub do
 
   defp valid_topic(%{"hub.topic" => topic}, user) do
     if topic == OStatus.feed_path(user) do
-      {:ok, topic}
+      {:ok, OStatus.feed_path(user)}
     else
       {:error, "Wrong topic requested, expected #{OStatus.feed_path(user)}, got #{topic}"}
     end
   end
+
+  def subscribe(subscriber, subscribed, requester \\ &request_subscription/1) do
+    topic = subscribed.info["topic"]
+    # FIXME: Race condition, use transactions
+    {:ok, subscription} = with subscription when not is_nil(subscription) <- Repo.get_by(WebsubClientSubscription, topic: topic) do
+      subscribers = [subscriber.ap_id, subscription.subscribers] |> Enum.uniq
+      change = Ecto.Changeset.change(subscription, %{subscribers: subscribers})
+      Repo.update(change)
+    else _e ->
+      subscription = %WebsubClientSubscription{
+        topic: topic,
+        hub: subscribed.info["hub"],
+        subscribers: [subscriber.ap_id],
+        state: "requested",
+        secret: :crypto.strong_rand_bytes(8) |> Base.url_encode64,
+        user: subscribed
+      }
+      Repo.insert(subscription)
+    end
+    requester.(subscription)
+  end
+
+  def gather_feed_data(topic, getter \\ &HTTPoison.get/1) do
+    with {:ok, response} <- getter.(topic),
+         status_code when status_code in 200..299 <- response.status_code,
+         body <- response.body,
+         doc <- XML.parse_document(body),
+         uri when not is_nil(uri) <- XML.string_from_xpath("/feed/author[1]/uri", doc),
+         hub when not is_nil(hub) <- XML.string_from_xpath(~S{/feed/link[@rel="hub"]/@href}, doc) do
+
+      name = XML.string_from_xpath("/feed/author[1]/name", doc)
+      preferredUsername = XML.string_from_xpath("/feed/author[1]/poco:preferredUsername", doc)
+      displayName = XML.string_from_xpath("/feed/author[1]/poco:displayName", doc)
+      avatar = OStatus.make_avatar_object(doc)
+
+      {:ok, %{
+        "uri" => uri,
+        "hub" => hub,
+        "nickname" => preferredUsername || name,
+        "name" => displayName || name,
+        "host" => URI.parse(uri).host,
+        "avatar" => avatar
+      }}
+    else e ->
+      {:error, e}
+    end
+  end
+
+  def request_subscription(websub, poster \\ &HTTPoison.post/3, timeout \\ 10_000) do
+    data = [
+      "hub.mode": "subscribe",
+      "hub.topic": websub.topic,
+      "hub.secret": websub.secret,
+      "hub.callback": Helpers.websub_url(Endpoint, :websub_subscription_confirmation, websub.id)
+    ]
+
+    # This checks once a second if we are confirmed yet
+    websub_checker = fn ->
+      helper = fn (helper) ->
+        :timer.sleep(1000)
+        websub = Repo.get_by(WebsubClientSubscription, id: websub.id, state: "accepted")
+        if websub, do: websub, else: helper.(helper)
+      end
+      helper.(helper)
+    end
+
+    task = Task.async(websub_checker)
+
+    with {:ok, %{status_code: 202}} <- poster.(websub.hub, {:form, data}, ["Content-type": "application/x-www-form-urlencoded"]),
+         {:ok, websub} <- Task.yield(task, timeout) do
+      {:ok, websub}
+    else e ->
+      Task.shutdown(task)
+
+      change = Ecto.Changeset.change(websub, %{state: "rejected"})
+      {:ok, websub} = Repo.update(change)
+
+      Logger.debug("Couldn't confirm subscription: #{inspect(websub)}")
+      Logger.debug("error: #{inspect(e)}")
+
+      {:error, websub}
+    end
+  end
 end
diff --git a/lib/pleroma/web/websub/websub_client_subscription.ex b/lib/pleroma/web/websub/websub_client_subscription.ex
new file mode 100644 (file)
index 0000000..c7a25ea
--- /dev/null
@@ -0,0 +1,16 @@
+defmodule Pleroma.Web.Websub.WebsubClientSubscription do
+  use Ecto.Schema
+  alias Pleroma.User
+
+  schema "websub_client_subscriptions" do
+    field :topic, :string
+    field :secret, :string
+    field :valid_until, :naive_datetime
+    field :state, :string
+    field :subscribers, {:array, :string}, default: []
+    field :hub, :string
+    belongs_to :user, User
+
+    timestamps()
+  end
+end
index 5d54c6ef5c936c10cf4ba128712f5dab93230c62..e860ec9e5381c180dde61df581a892cba7a345d3 100644 (file)
@@ -1,7 +1,11 @@
 defmodule Pleroma.Web.Websub.WebsubController do
   use Pleroma.Web, :controller
-  alias Pleroma.User
+  alias Pleroma.{Repo, User}
   alias Pleroma.Web.Websub
+  alias Pleroma.Web.Websub.WebsubClientSubscription
+  require Logger
+
+  @ostatus Application.get_env(:pleroma, :ostatus)
 
   def websub_subscription_request(conn, %{"nickname" => nickname} = params) do
     user = User.get_cached_by_nickname(nickname)
@@ -15,4 +19,32 @@ defmodule Pleroma.Web.Websub.WebsubController do
       |> send_resp(500, reason)
     end
   end
+
+  def websub_subscription_confirmation(conn, %{"id" => id, "hub.mode" => "subscribe", "hub.challenge" => challenge, "hub.topic" => topic}) do
+    with %WebsubClientSubscription{} = websub <- Repo.get_by(WebsubClientSubscription, id: id, topic: topic) do
+      change = Ecto.Changeset.change(websub, %{state: "accepted"})
+      {:ok, _websub} = Repo.update(change)
+      conn
+      |> send_resp(200, challenge)
+    else _e ->
+      conn
+      |> send_resp(500, "Error")
+    end
+  end
+
+  def websub_incoming(conn, %{"id" => id}) do
+    with "sha1=" <> signature <- hd(get_req_header(conn, "x-hub-signature")),
+         signature <- String.downcase(signature),
+         %WebsubClientSubscription{} = websub <- Repo.get(WebsubClientSubscription, id),
+         {:ok, body, _conn} = read_body(conn),
+         ^signature <- Websub.sign(websub.secret, body) do
+      @ostatus.handle_incoming(body)
+      conn
+      |> send_resp(200, "OK")
+    else _e ->
+      Logger.debug("Can't handle incoming subscription post")
+      conn
+      |> send_resp(500, "Error")
+    end
+  end
 end
diff --git a/lib/pleroma/web/xml/xml.ex b/lib/pleroma/web/xml/xml.ex
new file mode 100644 (file)
index 0000000..22faf72
--- /dev/null
@@ -0,0 +1,19 @@
+defmodule Pleroma.Web.XML do
+  def 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 parse_document(text) do
+    {doc, _rest} = text
+    |> :binary.bin_to_list
+    |> :xmerl_scan.string
+
+    doc
+  end
+end
diff --git a/priv/repo/migrations/20170423154511_add_fields_to_users.exs b/priv/repo/migrations/20170423154511_add_fields_to_users.exs
new file mode 100644 (file)
index 0000000..84de74b
--- /dev/null
@@ -0,0 +1,10 @@
+defmodule Pleroma.Repo.Migrations.AddFieldsToUsers do
+  use Ecto.Migration
+
+  def change do
+    alter table(:users) do
+      add :local, :boolean, default: true
+      add :info, :map
+    end
+  end
+end
diff --git a/priv/repo/migrations/20170426154155_create_websub_client_subscription.exs b/priv/repo/migrations/20170426154155_create_websub_client_subscription.exs
new file mode 100644 (file)
index 0000000..f427828
--- /dev/null
@@ -0,0 +1,15 @@
+defmodule Pleroma.Repo.Migrations.CreateWebsubClientSubscription do
+  use Ecto.Migration
+
+  def change do
+    create table(:websub_client_subscriptions) do
+      add :topic, :string
+      add :secret, :string
+      add :valid_until, :naive_datetime
+      add :state, :string
+      add :subscribers, :map
+
+      timestamps()
+    end
+  end
+end
diff --git a/priv/repo/migrations/20170427054757_add_user_and_hub.exs b/priv/repo/migrations/20170427054757_add_user_and_hub.exs
new file mode 100644 (file)
index 0000000..4f9a520
--- /dev/null
@@ -0,0 +1,10 @@
+defmodule Pleroma.Repo.Migrations.AddUserAndHub do
+  use Ecto.Migration
+
+  def change do
+    alter table(:websub_client_subscriptions) do
+      add :hub, :string
+      add :user_id, references(:users)
+    end
+  end
+end
diff --git a/priv/repo/migrations/20170501124823_add_id_contraints_to_activities_and_objects.exs b/priv/repo/migrations/20170501124823_add_id_contraints_to_activities_and_objects.exs
new file mode 100644 (file)
index 0000000..21534ad
--- /dev/null
@@ -0,0 +1,8 @@
+defmodule Pleroma.Repo.Migrations.AddIdContraintsToActivitiesAndObjects do
+  use Ecto.Migration
+
+  def change do
+    create index(:objects, ["(data->>\"id\")"], name: :objects_unique_apid_index)
+    create index(:activities, ["(data->>\"id\")"], name: :activities_unique_apid_index)
+  end
+end
diff --git a/priv/repo/migrations/20170501133231_add_id_contraints_to_activities_and_objects_part_two.exs b/priv/repo/migrations/20170501133231_add_id_contraints_to_activities_and_objects_part_two.exs
new file mode 100644 (file)
index 0000000..12eea13
--- /dev/null
@@ -0,0 +1,10 @@
+defmodule Pleroma.Repo.Migrations.AddIdContraintsToActivitiesAndObjectsPartTwo do
+  use Ecto.Migration
+
+  def change do
+    drop index(:objects, ["(data->>\"id\")"], name: :objects_unique_apid_index)
+    drop index(:activities, ["(data->>\"id\")"], name: :activities_unique_apid_index)
+    create unique_index(:objects, ["(data->>'id')"], name: :objects_unique_apid_index)
+    create unique_index(:activities, ["(data->>'id')"], name: :activities_unique_apid_index)
+  end
+end
diff --git a/priv/repo/migrations/20170502083023_add_local_field_to_activities.exs b/priv/repo/migrations/20170502083023_add_local_field_to_activities.exs
new file mode 100644 (file)
index 0000000..088d68f
--- /dev/null
@@ -0,0 +1,11 @@
+defmodule Pleroma.Repo.Migrations.AddLocalFieldToActivities do
+  use Ecto.Migration
+
+  def change do
+    alter table(:activities) do
+      add :local, :boolean, default: true
+    end
+
+    create index(:activities, [:local])
+  end
+end
index ce6eb15451f7ad7ba6b126d41a9d034b007532ae..366a2f9572290d139e81cd2563b4d1e42ee4f1ec 100644 (file)
@@ -15,4 +15,11 @@ defmodule Pleroma.ActivityTest do
 
     assert activity == found_activity
   end
+
+  test "returns the activity that created an object" do
+    activity = insert(:note_activity)
+    found_activity = Pleroma.Activity.get_create_activity_by_object_ap_id(activity.data["object"]["id"])
+
+    assert activity == found_activity
+  end
 end
diff --git a/test/fixtures/23211.atom b/test/fixtures/23211.atom
new file mode 100644 (file)
index 0000000..d5d111b
--- /dev/null
@@ -0,0 +1,508 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<feed xml:lang="en-US" xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0" xmlns:georss="http://www.georss.org/georss" xmlns:activity="http://activitystrea.ms/spec/1.0/" xmlns:media="http://purl.org/syndication/atommedia" xmlns:poco="http://portablecontacts.net/spec/1.0" xmlns:ostatus="http://ostatus.org/schema/1.0" xmlns:statusnet="http://status.net/schema/api/1/">
+ <generator uri="https://gnu.io/social" version="1.0.2-dev">GNU social</generator>
+ <id>https://social.heldscal.la/api/statuses/user_timeline/23211.atom</id>
+ <title>lambadalambda timeline</title>
+ <subtitle>Updates from lambadalambda on social.heldscal.la!</subtitle>
+ <logo>https://social.heldscal.la/avatar/23211-96-20170416114255.jpeg</logo>
+ <updated>2017-05-02T14:59:30+00:00</updated>
+<author>
+ <activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
+ <uri>https://social.heldscal.la/user/23211</uri>
+ <name>lambadalambda</name>
+ <summary>Call me Deacon Blues.</summary>
+ <link rel="alternate" type="text/html" href="https://social.heldscal.la/lambadalambda"/>
+ <link rel="avatar" type="image/jpeg" media:width="236" media:height="236" href="https://social.heldscal.la/avatar/23211-original-20170416114255.jpeg"/>
+ <link rel="avatar" type="image/jpeg" media:width="96" media:height="96" href="https://social.heldscal.la/avatar/23211-96-20170416114255.jpeg"/>
+ <link rel="avatar" type="image/jpeg" media:width="48" media:height="48" href="https://social.heldscal.la/avatar/23211-48-20170416114255.jpeg"/>
+ <link rel="avatar" type="image/jpeg" media:width="24" media:height="24" href="https://social.heldscal.la/avatar/23211-24-20170416114257.jpeg"/>
+ <poco:preferredUsername>lambadalambda</poco:preferredUsername>
+ <poco:displayName>Constance Variable</poco:displayName>
+ <poco:note>Call me Deacon Blues.</poco:note>
+ <poco:address>
+  <poco:formatted>Berlin</poco:formatted>
+ </poco:address>
+ <poco:urls>
+  <poco:type>homepage</poco:type>
+  <poco:value>https://heldscal.la</poco:value>
+  <poco:primary>true</poco:primary>
+ </poco:urls>
+ <followers url="https://social.heldscal.la/lambadalambda/subscribers"></followers>
+ <statusnet:profile_info local_id="23211"></statusnet:profile_info>
+</author>
+ <link href="https://social.heldscal.la/lambadalambda" rel="alternate" type="text/html"/>
+ <link href="https://social.heldscal.la/main/sup" rel="http://api.friendfeed.com/2008/03#sup" type="application/json"/>
+ <link href="https://social.heldscal.la/api/statuses/user_timeline/23211.atom?max_id=2012090" rel="next" type="application/atom+xml"/>
+ <link href="https://social.heldscal.la/main/push/hub" rel="hub"/>
+ <link href="https://social.heldscal.la/main/salmon/user/23211" rel="salmon"/>
+ <link href="https://social.heldscal.la/main/salmon/user/23211" rel="http://salmon-protocol.org/ns/salmon-replies"/>
+ <link href="https://social.heldscal.la/main/salmon/user/23211" rel="http://salmon-protocol.org/ns/salmon-mention"/>
+ <link href="https://social.heldscal.la/api/statuses/user_timeline/23211.atom" rel="self" type="application/atom+xml"/>
+<entry>
+ <id>tag:social.heldscal.la,2017-05-02:fave:23211:comment:2015260:2017-05-02T14:45:47+00:00</id>
+ <title>Favorite</title>
+ <content type="html">lambadalambda favorited something by godemperorofdune: &lt;p&gt;&lt;span class=&quot;h-card&quot;&gt;&lt;a href=&quot;https://social.heldscal.la/lambadalambda&quot; class=&quot;u-url mention&quot;&gt;@&lt;span&gt;lambadalambda&lt;/span&gt;&lt;/a&gt;&lt;/span&gt; It's because your instance decided to be trap! lol.&lt;/p&gt;</content>
+ <link rel="alternate" type="text/html" href="https://social.heldscal.la/notice/2015305"/>
+ <activity:verb>http://activitystrea.ms/schema/1.0/favorite</activity:verb>
+ <published>2017-05-02T14:45:47+00:00</published>
+ <updated>2017-05-02T14:45:47+00:00</updated>
+ <activity:object>
+  <activity:object-type>http://activitystrea.ms/schema/1.0/comment</activity:object-type>
+  <id>tag:pawoo.net,2017-05-02:objectId=7397439:objectType=Status</id>
+  <title>New comment by godemperorofdune</title>
+  <content type="html">&lt;p&gt;&lt;span class=&quot;h-card&quot;&gt;&lt;a href=&quot;https://social.heldscal.la/lambadalambda&quot; class=&quot;u-url mention&quot;&gt;@&lt;span&gt;lambadalambda&lt;/span&gt;&lt;/a&gt;&lt;/span&gt; It's because your instance decided to be trap! lol.&lt;/p&gt;</content>
+  <link rel="alternate" type="text/html" href="https://pawoo.net/users/God_Emperor_of_Dune/updates/2090090"/>
+  <status_net notice_id="2015260"></status_net>
+ </activity:object>
+ <thr:in-reply-to ref="tag:pawoo.net,2017-05-02:objectId=7397439:objectType=Status" href="https://pawoo.net/users/God_Emperor_of_Dune/updates/2090090"></thr:in-reply-to>
+ <link rel="related" href="https://pawoo.net/users/God_Emperor_of_Dune/updates/2090090"/>
+ <link rel="ostatus:conversation" href="https://social.heldscal.la/conversation/1035308"/>
+ <ostatus:conversation href="https://social.heldscal.la/conversation/1035308" local_id="1035308" ref="tag:social.heldscal.la,2017-05-02:objectType=thread:nonce=136e244b26cdf1e9">tag:social.heldscal.la,2017-05-02:objectType=thread:nonce=136e244b26cdf1e9</ostatus:conversation>
+ <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
+ <link rel="self" type="application/atom+xml" href="https://social.heldscal.la/api/statuses/show/2015305.atom"/>
+ <link rel="edit" type="application/atom+xml" href="https://social.heldscal.la/api/statuses/show/2015305.atom"/>
+ <statusnet:notice_info local_id="2015305" source="unknown"></statusnet:notice_info>
+</entry>
+<entry>
+ <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
+ <id>tag:social.heldscal.la,2017-05-02:noticeId=2015221:objectType=note</id>
+ <title>New note by lambadalambda</title>
+ <content type="html">Some script thinks I'm a mastodon server.&lt;br /&gt; &lt;br /&gt; [info] GET /api/v1/timelines/public&lt;br /&gt; [debug] Processing with Fallback.RedirectController.redirector/2&lt;br /&gt; Parameters: %{&amp;quot;limit&amp;quot; =&amp;gt; &amp;quot;40&amp;quot;, &amp;quot;path&amp;quot; =&amp;gt; [&amp;quot;api&amp;quot;, &amp;quot;v1&amp;quot;, &amp;quot;timelines&amp;quot;, &amp;quot;public&amp;quot;]}&lt;br /&gt; Pipelines: []</content>
+ <link rel="alternate" type="text/html" href="https://social.heldscal.la/notice/2015221"/>
+ <status_net notice_id="2015221"></status_net>
+ <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
+ <published>2017-05-02T14:40:50+00:00</published>
+ <updated>2017-05-02T14:40:50+00:00</updated>
+ <link rel="ostatus:conversation" href="https://social.heldscal.la/conversation/1035308"/>
+ <ostatus:conversation href="https://social.heldscal.la/conversation/1035308" local_id="1035308" ref="tag:social.heldscal.la,2017-05-02:objectType=thread:nonce=136e244b26cdf1e9">tag:social.heldscal.la,2017-05-02:objectType=thread:nonce=136e244b26cdf1e9</ostatus:conversation>
+ <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
+ <link rel="self" type="application/atom+xml" href="https://social.heldscal.la/api/statuses/show/2015221.atom"/>
+ <link rel="edit" type="application/atom+xml" href="https://social.heldscal.la/api/statuses/show/2015221.atom"/>
+ <statusnet:notice_info local_id="2015221" source="Pleroma FE"></statusnet:notice_info>
+</entry>
+<entry>
+ <activity:object-type>http://activitystrea.ms/schema/1.0/comment</activity:object-type>
+ <id>tag:social.heldscal.la,2017-05-02:noticeId=2014759:objectType=comment</id>
+ <title>New comment by lambadalambda</title>
+ <content type="html">@&lt;a href=&quot;https://mstdn.io/users/mattskala&quot; class=&quot;h-card u-url p-nickname mention&quot; title=&quot;Matthew Skala&quot;&gt;mattskala&lt;/a&gt; You and @&lt;a href=&quot;https://mastodon.social/users/kevinmarks&quot; class=&quot;h-card u-url p-nickname mention&quot; title=&quot;Kevin Marks&quot;&gt;kevinmarks&lt;/a&gt; are not wrong, but my comment was a suggestion to users and admins: Don't use big instances, don't run big instances. Also, it's a secondary advice to devs: Don't add features that encourage big instances.</content>
+ <link rel="alternate" type="text/html" href="https://social.heldscal.la/notice/2014759"/>
+ <status_net notice_id="2014759"></status_net>
+ <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
+ <published>2017-05-02T14:11:54+00:00</published>
+ <updated>2017-05-02T14:11:54+00:00</updated>
+ <thr:in-reply-to ref="tag:mstdn.io,2017-05-02:objectId=1316931:objectType=Status" href="https://mstdn.io/users/mattskala/updates/35698"></thr:in-reply-to>
+ <link rel="related" href="https://mstdn.io/users/mattskala/updates/35698"/>
+ <link rel="ostatus:conversation" href="https://social.heldscal.la/conversation/1031866"/>
+ <ostatus:conversation href="https://social.heldscal.la/conversation/1031866" local_id="1031866" ref="tag:social.heldscal.la,2017-05-02:objectType=thread:nonce=58e32e013ab6487d">tag:social.heldscal.la,2017-05-02:objectType=thread:nonce=58e32e013ab6487d</ostatus:conversation>
+ <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="https://mastodon.social/users/kevinmarks"/>
+ <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="https://mstdn.io/users/mattskala"/>
+ <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
+ <link rel="self" type="application/atom+xml" href="https://social.heldscal.la/api/statuses/show/2014759.atom"/>
+ <link rel="edit" type="application/atom+xml" href="https://social.heldscal.la/api/statuses/show/2014759.atom"/>
+ <statusnet:notice_info local_id="2014759" source="Pleroma FE"></statusnet:notice_info>
+</entry>
+<entry>
+ <activity:object-type>http://activitystrea.ms/schema/1.0/comment</activity:object-type>
+ <id>tag:social.heldscal.la,2017-05-02:noticeId=2014684:objectType=comment</id>
+ <title>New comment by lambadalambda</title>
+ <content type="html">@&lt;a href=&quot;https://mastodon.social/users/Ronkjeffries&quot; class=&quot;h-card u-url p-nickname mention&quot; title=&quot;Ron K Jeffries social&quot;&gt;ronkjeffries&lt;/a&gt; @&lt;a href=&quot;https://xoxo.zone/users/KevinMarks&quot; class=&quot;h-card u-url p-nickname mention&quot; title=&quot;Kevin Marks &quot;&gt;kevinmarks&lt;/a&gt; Usually people who run their own private instance just look at the timelines of other servers, follow a seed population and then go from there. This is of course hard on Mastodon, because it doesn't have a publicly visible timeline.</content>
+ <link rel="alternate" type="text/html" href="https://social.heldscal.la/notice/2014684"/>
+ <status_net notice_id="2014684"></status_net>
+ <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
+ <published>2017-05-02T14:07:00+00:00</published>
+ <updated>2017-05-02T14:07:00+00:00</updated>
+ <thr:in-reply-to ref="tag:mastodon.social,2017-05-02:objectId=4883853:objectType=Status" href="https://mastodon.social/users/Ronkjeffries/updates/2221244"></thr:in-reply-to>
+ <link rel="related" href="https://mastodon.social/users/Ronkjeffries/updates/2221244"/>
+ <link rel="ostatus:conversation" href="https://social.heldscal.la/conversation/1031866"/>
+ <ostatus:conversation href="https://social.heldscal.la/conversation/1031866" local_id="1031866" ref="tag:social.heldscal.la,2017-05-02:objectType=thread:nonce=58e32e013ab6487d">tag:social.heldscal.la,2017-05-02:objectType=thread:nonce=58e32e013ab6487d</ostatus:conversation>
+ <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="https://xoxo.zone/users/KevinMarks"/>
+ <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="https://mastodon.social/users/Ronkjeffries"/>
+ <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
+ <link rel="self" type="application/atom+xml" href="https://social.heldscal.la/api/statuses/show/2014684.atom"/>
+ <link rel="edit" type="application/atom+xml" href="https://social.heldscal.la/api/statuses/show/2014684.atom"/>
+ <statusnet:notice_info local_id="2014684" source="Pleroma FE"></statusnet:notice_info>
+</entry>
+<entry>
+ <id>tag:social.heldscal.la,2017-05-02:fave:23211:comment:2014584:2017-05-02T14:05:32+00:00</id>
+ <title>Favorite</title>
+ <content type="html">lambadalambda favorited something by mattskala: &lt;p&gt;&lt;span class=&quot;h-card&quot;&gt;&lt;a href=&quot;https://social.heldscal.la/lambadalambda&quot; class=&quot;u-url mention&quot;&gt;@&lt;span&gt;lambadalambda&lt;/span&gt;&lt;/a&gt;&lt;/span&gt; It's reasonable to expect that instance sizes will obey a power-law distribution because that's what such things in nature nearly always do.  If so, there'll necessarily be a few instances much larger than the others; even if most are small, the network both socially and technically has to be able to deal with the existence of the few large ones.&lt;/p&gt;</content>
+ <link rel="alternate" type="text/html" href="https://social.heldscal.la/notice/2014659"/>
+ <activity:verb>http://activitystrea.ms/schema/1.0/favorite</activity:verb>
+ <published>2017-05-02T14:05:32+00:00</published>
+ <updated>2017-05-02T14:05:32+00:00</updated>
+ <activity:object>
+  <activity:object-type>http://activitystrea.ms/schema/1.0/comment</activity:object-type>
+  <id>tag:mstdn.io,2017-05-02:objectId=1316931:objectType=Status</id>
+  <title>New comment by mattskala</title>
+  <content type="html">&lt;p&gt;&lt;span class=&quot;h-card&quot;&gt;&lt;a href=&quot;https://social.heldscal.la/lambadalambda&quot; class=&quot;u-url mention&quot;&gt;@&lt;span&gt;lambadalambda&lt;/span&gt;&lt;/a&gt;&lt;/span&gt; It's reasonable to expect that instance sizes will obey a power-law distribution because that's what such things in nature nearly always do.  If so, there'll necessarily be a few instances much larger than the others; even if most are small, the network both socially and technically has to be able to deal with the existence of the few large ones.&lt;/p&gt;</content>
+  <link rel="alternate" type="text/html" href="https://mstdn.io/users/mattskala/updates/35698"/>
+  <status_net notice_id="2014584"></status_net>
+ </activity:object>
+ <thr:in-reply-to ref="tag:mstdn.io,2017-05-02:objectId=1316931:objectType=Status" href="https://mstdn.io/users/mattskala/updates/35698"></thr:in-reply-to>
+ <link rel="related" href="https://mstdn.io/users/mattskala/updates/35698"/>
+ <link rel="ostatus:conversation" href="https://social.heldscal.la/conversation/1031866"/>
+ <ostatus:conversation href="https://social.heldscal.la/conversation/1031866" local_id="1031866" ref="tag:social.heldscal.la,2017-05-02:objectType=thread:nonce=58e32e013ab6487d">tag:social.heldscal.la,2017-05-02:objectType=thread:nonce=58e32e013ab6487d</ostatus:conversation>
+ <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
+ <link rel="self" type="application/atom+xml" href="https://social.heldscal.la/api/statuses/show/2014659.atom"/>
+ <link rel="edit" type="application/atom+xml" href="https://social.heldscal.la/api/statuses/show/2014659.atom"/>
+ <statusnet:notice_info local_id="2014659" source="unknown"></statusnet:notice_info>
+</entry>
+<entry>
+ <id>tag:social.heldscal.la,2017-05-02:fave:23211:comment:2013568:2017-05-02T14:05:29+00:00</id>
+ <title>Favorite</title>
+ <content type="html">lambadalambda favorited something by kevinmarks: &lt;p&gt;&lt;span class=&quot;h-card&quot;&gt;&lt;a href=&quot;https://social.heldscal.la/lambadalambda&quot; class=&quot;u-url mention&quot;&gt;@&lt;span&gt;lambadalambda&lt;/span&gt;&lt;/a&gt;&lt;/span&gt; except instance populations will be power law distributed, and the problems for the tummlers are worse at scale&lt;/p&gt;</content>
+ <link rel="alternate" type="text/html" href="https://social.heldscal.la/notice/2014657"/>
+ <activity:verb>http://activitystrea.ms/schema/1.0/favorite</activity:verb>
+ <published>2017-05-02T14:05:29+00:00</published>
+ <updated>2017-05-02T14:05:29+00:00</updated>
+ <activity:object>
+  <activity:object-type>http://activitystrea.ms/schema/1.0/comment</activity:object-type>
+  <id>tag:xoxo.zone,2017-05-02:objectId=89478:objectType=Status</id>
+  <title>New comment by kevinmarks</title>
+  <content type="html">&lt;p&gt;&lt;span class=&quot;h-card&quot;&gt;&lt;a href=&quot;https://social.heldscal.la/lambadalambda&quot; class=&quot;u-url mention&quot;&gt;@&lt;span&gt;lambadalambda&lt;/span&gt;&lt;/a&gt;&lt;/span&gt; except instance populations will be power law distributed, and the problems for the tummlers are worse at scale&lt;/p&gt;</content>
+  <link rel="alternate" type="text/html" href="https://xoxo.zone/users/KevinMarks/updates/1749"/>
+  <status_net notice_id="2013568"></status_net>
+ </activity:object>
+ <thr:in-reply-to ref="tag:xoxo.zone,2017-05-02:objectId=89478:objectType=Status" href="https://xoxo.zone/users/KevinMarks/updates/1749"></thr:in-reply-to>
+ <link rel="related" href="https://xoxo.zone/users/KevinMarks/updates/1749"/>
+ <link rel="ostatus:conversation" href="https://social.heldscal.la/conversation/1031866"/>
+ <ostatus:conversation href="https://social.heldscal.la/conversation/1031866" local_id="1031866" ref="tag:social.heldscal.la,2017-05-02:objectType=thread:nonce=58e32e013ab6487d">tag:social.heldscal.la,2017-05-02:objectType=thread:nonce=58e32e013ab6487d</ostatus:conversation>
+ <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
+ <link rel="self" type="application/atom+xml" href="https://social.heldscal.la/api/statuses/show/2014657.atom"/>
+ <link rel="edit" type="application/atom+xml" href="https://social.heldscal.la/api/statuses/show/2014657.atom"/>
+ <statusnet:notice_info local_id="2014657" source="unknown"></statusnet:notice_info>
+</entry>
+<entry>
+ <id>tag:social.heldscal.la,2017-05-02:fave:23211:comment:2014060:2017-05-02T13:34:32+00:00</id>
+ <title>Favorite</title>
+ <content type="html">lambadalambda favorited something by gcarregues: &lt;p&gt;&lt;span class=&quot;h-card&quot;&gt;&lt;a href=&quot;https://social.heldscal.la/lambadalambda&quot; class=&quot;u-url mention&quot;&gt;@&lt;span&gt;lambadalambda&lt;/span&gt;&lt;/a&gt;&lt;/span&gt; Oh purée ! Ma vie en images !&lt;/p&gt;</content>
+ <link rel="alternate" type="text/html" href="https://social.heldscal.la/notice/2014147"/>
+ <activity:verb>http://activitystrea.ms/schema/1.0/favorite</activity:verb>
+ <published>2017-05-02T13:34:32+00:00</published>
+ <updated>2017-05-02T13:34:32+00:00</updated>
+ <activity:object>
+  <activity:object-type>http://activitystrea.ms/schema/1.0/comment</activity:object-type>
+  <id>tag:mastodon.etalab.gouv.fr,2017-05-02:objectId=55287:objectType=Status</id>
+  <title>New comment by gcarregues</title>
+  <content type="html">&lt;p&gt;&lt;span class=&quot;h-card&quot;&gt;&lt;a href=&quot;https://social.heldscal.la/lambadalambda&quot; class=&quot;u-url mention&quot;&gt;@&lt;span&gt;lambadalambda&lt;/span&gt;&lt;/a&gt;&lt;/span&gt; Oh purée ! Ma vie en images !&lt;/p&gt;</content>
+  <link rel="alternate" type="text/html" href="https://mastodon.etalab.gouv.fr/users/gcarregues/updates/4370"/>
+  <status_net notice_id="2014060"></status_net>
+ </activity:object>
+ <thr:in-reply-to ref="tag:mastodon.etalab.gouv.fr,2017-05-02:objectId=55287:objectType=Status" href="https://mastodon.etalab.gouv.fr/users/gcarregues/updates/4370"></thr:in-reply-to>
+ <link rel="related" href="https://mastodon.etalab.gouv.fr/users/gcarregues/updates/4370"/>
+ <link rel="ostatus:conversation" href="https://social.heldscal.la/conversation/1034065"/>
+ <ostatus:conversation href="https://social.heldscal.la/conversation/1034065" local_id="1034065" ref="tag:social.heldscal.la,2017-05-02:objectType=thread:nonce=2c27c27df8ec4dcc">tag:social.heldscal.la,2017-05-02:objectType=thread:nonce=2c27c27df8ec4dcc</ostatus:conversation>
+ <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
+ <link rel="self" type="application/atom+xml" href="https://social.heldscal.la/api/statuses/show/2014147.atom"/>
+ <link rel="edit" type="application/atom+xml" href="https://social.heldscal.la/api/statuses/show/2014147.atom"/>
+ <statusnet:notice_info local_id="2014147" source="unknown"></statusnet:notice_info>
+</entry>
+<entry>
+ <id>tag:social.heldscal.la,2017-05-02:fave:23211:note:2013573:2017-05-02T13:03:33+00:00</id>
+ <title>Favorite</title>
+ <content type="html">lambadalambda favorited something by phildobangnz: also @&lt;a href=&quot;https://sealion.club/user/579&quot; class=&quot;h-card mention&quot; title=&quot;Sim Bot&quot;&gt;sim&lt;/a&gt; reminder you are awesome; don't even trip- u kewler than Tutankhamen's cucumber, fam. Okay, good night.</content>
+ <link rel="alternate" type="text/html" href="https://social.heldscal.la/notice/2013702"/>
+ <activity:verb>http://activitystrea.ms/schema/1.0/favorite</activity:verb>
+ <published>2017-05-02T13:03:33+00:00</published>
+ <updated>2017-05-02T13:03:33+00:00</updated>
+ <activity:object>
+  <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
+  <id>tag:sealion.club,2017-05-02:noticeId=3060818:objectType=note</id>
+  <title>New note by phildobangnz</title>
+  <content type="html">also @&lt;a href=&quot;https://sealion.club/user/579&quot; class=&quot;h-card mention&quot; title=&quot;Sim Bot&quot;&gt;sim&lt;/a&gt; reminder you are awesome; don't even trip- u kewler than Tutankhamen's cucumber, fam. Okay, good night.</content>
+  <link rel="alternate" type="text/html" href="https://sealion.club/notice/3060818"/>
+  <status_net notice_id="2013573"></status_net>
+ </activity:object>
+ <thr:in-reply-to ref="tag:sealion.club,2017-05-02:noticeId=3060818:objectType=note" href="https://sealion.club/notice/3060818"></thr:in-reply-to>
+ <link rel="related" href="https://sealion.club/notice/3060818"/>
+ <link rel="ostatus:conversation" href="https://social.heldscal.la/conversation/1034282"/>
+ <ostatus:conversation href="https://social.heldscal.la/conversation/1034282" local_id="1034282" ref="https://sealion.club/conversation/1633267">https://sealion.club/conversation/1633267</ostatus:conversation>
+ <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
+ <link rel="self" type="application/atom+xml" href="https://social.heldscal.la/api/statuses/show/2013702.atom"/>
+ <link rel="edit" type="application/atom+xml" href="https://social.heldscal.la/api/statuses/show/2013702.atom"/>
+ <statusnet:notice_info local_id="2013702" source="unknown"></statusnet:notice_info>
+</entry>
+<entry>
+ <activity:object-type>http://activitystrea.ms/schema/1.0/comment</activity:object-type>
+ <id>tag:social.heldscal.la,2017-05-02:noticeId=2013586:objectType=comment</id>
+ <title>New comment by lambadalambda</title>
+ <content type="html">@&lt;a href=&quot;https://xoxo.zone/users/KevinMarks&quot; class=&quot;h-card u-url p-nickname mention&quot; title=&quot;Kevin Marks &quot;&gt;kevinmarks&lt;/a&gt; People can stay in their giant unmoderatable instances with meaningless public and federated timelines and experience constant federation drama if they want. I'll stay here with my 5 friends.</content>
+ <link rel="alternate" type="text/html" href="https://social.heldscal.la/notice/2013586"/>
+ <status_net notice_id="2013586"></status_net>
+ <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
+ <published>2017-05-02T12:54:59+00:00</published>
+ <updated>2017-05-02T12:54:59+00:00</updated>
+ <thr:in-reply-to ref="tag:xoxo.zone,2017-05-02:objectId=89478:objectType=Status" href="https://xoxo.zone/users/KevinMarks/updates/1749"></thr:in-reply-to>
+ <link rel="related" href="https://xoxo.zone/users/KevinMarks/updates/1749"/>
+ <link rel="ostatus:conversation" href="https://social.heldscal.la/conversation/1031866"/>
+ <ostatus:conversation href="https://social.heldscal.la/conversation/1031866" local_id="1031866" ref="tag:social.heldscal.la,2017-05-02:objectType=thread:nonce=58e32e013ab6487d">tag:social.heldscal.la,2017-05-02:objectType=thread:nonce=58e32e013ab6487d</ostatus:conversation>
+ <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="https://xoxo.zone/users/KevinMarks"/>
+ <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
+ <link rel="self" type="application/atom+xml" href="https://social.heldscal.la/api/statuses/show/2013586.atom"/>
+ <link rel="edit" type="application/atom+xml" href="https://social.heldscal.la/api/statuses/show/2013586.atom"/>
+ <statusnet:notice_info local_id="2013586" source="Pleroma FE"></statusnet:notice_info>
+</entry>
+<entry>
+ <id>tag:social.heldscal.la,2017-05-02:fave:23211:note:2013486:2017-05-02T12:46:48+00:00</id>
+ <title>Favorite</title>
+ <content type="html">lambadalambda favorited something by fortune: There once was a dentist named Stone&lt;br /&gt; Who saw all his patients alone.&lt;br /&gt; In a fit of depravity&lt;br /&gt; He filled the wrong cavity,&lt;br /&gt; And my, how his practice has grown!</content>
+ <link rel="alternate" type="text/html" href="https://social.heldscal.la/notice/2013511"/>
+ <activity:verb>http://activitystrea.ms/schema/1.0/favorite</activity:verb>
+ <published>2017-05-02T12:46:48+00:00</published>
+ <updated>2017-05-02T12:46:48+00:00</updated>
+ <activity:object>
+  <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
+  <id>tag:gs.kawa-kun.com,2017-05-02:noticeId=1655658:objectType=note</id>
+  <title>New note by fortune</title>
+  <content type="html">There once was a dentist named Stone&lt;br /&gt; Who saw all his patients alone.&lt;br /&gt; In a fit of depravity&lt;br /&gt; He filled the wrong cavity,&lt;br /&gt; And my, how his practice has grown!</content>
+  <link rel="alternate" type="text/html" href="https://gs.kawa-kun.com/notice/1655658"/>
+  <status_net notice_id="2013486"></status_net>
+ </activity:object>
+ <thr:in-reply-to ref="tag:gs.kawa-kun.com,2017-05-02:noticeId=1655658:objectType=note" href="https://gs.kawa-kun.com/notice/1655658"></thr:in-reply-to>
+ <link rel="related" href="https://gs.kawa-kun.com/notice/1655658"/>
+ <link rel="ostatus:conversation" href="https://social.heldscal.la/conversation/1034222"/>
+ <ostatus:conversation href="https://social.heldscal.la/conversation/1034222" local_id="1034222" ref="https://gs.kawa-kun.com/conversation/714072">https://gs.kawa-kun.com/conversation/714072</ostatus:conversation>
+ <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
+ <link rel="self" type="application/atom+xml" href="https://social.heldscal.la/api/statuses/show/2013511.atom"/>
+ <link rel="edit" type="application/atom+xml" href="https://social.heldscal.la/api/statuses/show/2013511.atom"/>
+ <statusnet:notice_info local_id="2013511" source="unknown"></statusnet:notice_info>
+</entry>
+<entry>
+ <id>tag:social.heldscal.la,2017-05-02:fave:23211:note:2013365:2017-05-02T12:37:55+00:00</id>
+ <title>Favorite</title>
+ <content type="html">lambadalambda favorited something by xj9: &lt;p&gt;&amp;gt; rollerblading to work&lt;/p&gt;</content>
+ <link rel="alternate" type="text/html" href="https://social.heldscal.la/notice/2013394"/>
+ <activity:verb>http://activitystrea.ms/schema/1.0/favorite</activity:verb>
+ <published>2017-05-02T12:37:55+00:00</published>
+ <updated>2017-05-02T12:37:55+00:00</updated>
+ <activity:object>
+  <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
+  <id>tag:sunshinegardens.org,2017-05-02:objectId=61020:objectType=Status</id>
+  <title>New note by xj9</title>
+  <content type="html">&lt;p&gt;&amp;gt; rollerblading to work&lt;/p&gt;</content>
+  <link rel="alternate" type="text/html" href="https://sunshinegardens.org/users/xj9/updates/748"/>
+  <status_net notice_id="2013365"></status_net>
+ </activity:object>
+ <thr:in-reply-to ref="tag:sunshinegardens.org,2017-05-02:objectId=61020:objectType=Status" href="https://sunshinegardens.org/users/xj9/updates/748"></thr:in-reply-to>
+ <link rel="related" href="https://sunshinegardens.org/users/xj9/updates/748"/>
+ <link rel="ostatus:conversation" href="https://social.heldscal.la/conversation/1034152"/>
+ <ostatus:conversation href="https://social.heldscal.la/conversation/1034152" local_id="1034152" ref="tag:social.heldscal.la,2017-05-02:objectType=thread:nonce=5a0e98612f634218">tag:social.heldscal.la,2017-05-02:objectType=thread:nonce=5a0e98612f634218</ostatus:conversation>
+ <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
+ <link rel="self" type="application/atom+xml" href="https://social.heldscal.la/api/statuses/show/2013394.atom"/>
+ <link rel="edit" type="application/atom+xml" href="https://social.heldscal.la/api/statuses/show/2013394.atom"/>
+ <statusnet:notice_info local_id="2013394" source="unknown"></statusnet:notice_info>
+</entry>
+<entry>
+ <id>tag:social.heldscal.la,2017-05-02:fave:23211:comment:2013259:2017-05-02T12:29:03+00:00</id>
+ <title>Favorite</title>
+ <content type="html">lambadalambda favorited something by cereal: @&lt;a href=&quot;https://gs.smuglo.li/user/28250&quot; class=&quot;h-card mention&quot; title=&quot;Bricky&quot;&gt;thatbrickster&lt;/a&gt; @&lt;a href=&quot;https://social.heldscal.la/user/23211&quot; class=&quot;h-card mention&quot; title=&quot;Constance Variable&quot;&gt;lambadalambda&lt;/a&gt; But why?</content>
+ <link rel="alternate" type="text/html" href="https://social.heldscal.la/notice/2013267"/>
+ <activity:verb>http://activitystrea.ms/schema/1.0/favorite</activity:verb>
+ <published>2017-05-02T12:29:03+00:00</published>
+ <updated>2017-05-02T12:29:03+00:00</updated>
+ <activity:object>
+  <activity:object-type>http://activitystrea.ms/schema/1.0/comment</activity:object-type>
+  <id>tag:sealion.club,2017-05-02:noticeId=3059985:objectType=comment</id>
+  <title>New comment by cereal</title>
+  <content type="html">@&lt;a href=&quot;https://gs.smuglo.li/user/28250&quot; class=&quot;h-card mention&quot; title=&quot;Bricky&quot;&gt;thatbrickster&lt;/a&gt; @&lt;a href=&quot;https://social.heldscal.la/user/23211&quot; class=&quot;h-card mention&quot; title=&quot;Constance Variable&quot;&gt;lambadalambda&lt;/a&gt; But why?</content>
+  <link rel="alternate" type="text/html" href="https://sealion.club/notice/3059985"/>
+  <status_net notice_id="2013259"></status_net>
+ </activity:object>
+ <thr:in-reply-to ref="tag:sealion.club,2017-05-02:noticeId=3059985:objectType=comment" href="https://sealion.club/notice/3059985"></thr:in-reply-to>
+ <link rel="related" href="https://sealion.club/notice/3059985"/>
+ <link rel="ostatus:conversation" href="https://social.heldscal.la/conversation/1034065"/>
+ <ostatus:conversation href="https://social.heldscal.la/conversation/1034065" local_id="1034065" ref="tag:social.heldscal.la,2017-05-02:objectType=thread:nonce=2c27c27df8ec4dcc">tag:social.heldscal.la,2017-05-02:objectType=thread:nonce=2c27c27df8ec4dcc</ostatus:conversation>
+ <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
+ <link rel="self" type="application/atom+xml" href="https://social.heldscal.la/api/statuses/show/2013267.atom"/>
+ <link rel="edit" type="application/atom+xml" href="https://social.heldscal.la/api/statuses/show/2013267.atom"/>
+ <statusnet:notice_info local_id="2013267" source="unknown"></statusnet:notice_info>
+</entry>
+<entry>
+ <id>tag:social.heldscal.la,2017-05-02:fave:23211:comment:2013227:2017-05-02T12:24:27+00:00</id>
+ <title>Favorite</title>
+ <content type="html">lambadalambda favorited something by thatbrickster: @&lt;a href=&quot;https://social.heldscal.la/user/23211&quot; class=&quot;h-card u-url p-nickname mention&quot; title=&quot;Constance Variable&quot;&gt;lambadalambda&lt;/a&gt; install gentoo</content>
+ <link rel="alternate" type="text/html" href="https://social.heldscal.la/notice/2013230"/>
+ <activity:verb>http://activitystrea.ms/schema/1.0/favorite</activity:verb>
+ <published>2017-05-02T12:24:27+00:00</published>
+ <updated>2017-05-02T12:24:27+00:00</updated>
+ <activity:object>
+  <activity:object-type>http://activitystrea.ms/schema/1.0/comment</activity:object-type>
+  <id>tag:gs.smuglo.li,2017-05-02:noticeId=2144296:objectType=comment</id>
+  <title>New comment by thatbrickster</title>
+  <content type="html">@&lt;a href=&quot;https://social.heldscal.la/user/23211&quot; class=&quot;h-card u-url p-nickname mention&quot; title=&quot;Constance Variable&quot;&gt;lambadalambda&lt;/a&gt; install gentoo</content>
+  <link rel="alternate" type="text/html" href="https://gs.smuglo.li/notice/2144296"/>
+  <status_net notice_id="2013227"></status_net>
+ </activity:object>
+ <thr:in-reply-to ref="tag:gs.smuglo.li,2017-05-02:noticeId=2144296:objectType=comment" href="https://gs.smuglo.li/notice/2144296"></thr:in-reply-to>
+ <link rel="related" href="https://gs.smuglo.li/notice/2144296"/>
+ <link rel="ostatus:conversation" href="https://social.heldscal.la/conversation/1034065"/>
+ <ostatus:conversation href="https://social.heldscal.la/conversation/1034065" local_id="1034065" ref="tag:social.heldscal.la,2017-05-02:objectType=thread:nonce=2c27c27df8ec4dcc">tag:social.heldscal.la,2017-05-02:objectType=thread:nonce=2c27c27df8ec4dcc</ostatus:conversation>
+ <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
+ <link rel="self" type="application/atom+xml" href="https://social.heldscal.la/api/statuses/show/2013230.atom"/>
+ <link rel="edit" type="application/atom+xml" href="https://social.heldscal.la/api/statuses/show/2013230.atom"/>
+ <statusnet:notice_info local_id="2013230" source="unknown"></statusnet:notice_info>
+</entry>
+<entry>
+ <id>tag:social.heldscal.la,2017-05-02:fave:23211:comment:2013213:2017-05-02T12:22:53+00:00</id>
+ <title>Favorite</title>
+ <content type="html">lambadalambda favorited something by dwmatiz: @&lt;a href=&quot;https://social.heldscal.la/user/23211&quot; class=&quot;h-card mention&quot;&gt;lambadalambda&lt;/a&gt; *unzips dick*</content>
+ <link rel="alternate" type="text/html" href="https://social.heldscal.la/notice/2013218"/>
+ <activity:verb>http://activitystrea.ms/schema/1.0/favorite</activity:verb>
+ <published>2017-05-02T12:22:53+00:00</published>
+ <updated>2017-05-02T12:22:53+00:00</updated>
+ <activity:object>
+  <activity:object-type>http://activitystrea.ms/schema/1.0/comment</activity:object-type>
+  <id>tag:sealion.club,2017-05-02:noticeId=3059800:objectType=comment</id>
+  <title>New comment by dwmatiz</title>
+  <content type="html">@&lt;a href=&quot;https://social.heldscal.la/user/23211&quot; class=&quot;h-card mention&quot;&gt;lambadalambda&lt;/a&gt; *unzips dick*</content>
+  <link rel="alternate" type="text/html" href="https://sealion.club/notice/3059800"/>
+  <status_net notice_id="2013213"></status_net>
+ </activity:object>
+ <thr:in-reply-to ref="tag:sealion.club,2017-05-02:noticeId=3059800:objectType=comment" href="https://sealion.club/notice/3059800"></thr:in-reply-to>
+ <link rel="related" href="https://sealion.club/notice/3059800"/>
+ <link rel="ostatus:conversation" href="https://social.heldscal.la/conversation/1034065"/>
+ <ostatus:conversation href="https://social.heldscal.la/conversation/1034065" local_id="1034065" ref="tag:social.heldscal.la,2017-05-02:objectType=thread:nonce=2c27c27df8ec4dcc">tag:social.heldscal.la,2017-05-02:objectType=thread:nonce=2c27c27df8ec4dcc</ostatus:conversation>
+ <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
+ <link rel="self" type="application/atom+xml" href="https://social.heldscal.la/api/statuses/show/2013218.atom"/>
+ <link rel="edit" type="application/atom+xml" href="https://social.heldscal.la/api/statuses/show/2013218.atom"/>
+ <statusnet:notice_info local_id="2013218" source="unknown"></statusnet:notice_info>
+</entry>
+<entry>
+ <id>tag:social.heldscal.la,2017-05-02:fave:23211:comment:2013199:2017-05-02T12:22:03+00:00</id>
+ <title>Favorite</title>
+ <content type="html">lambadalambda favorited something by shpuld: @&lt;a href=&quot;https://social.heldscal.la/user/23211&quot; class=&quot;h-card mention&quot; title=&quot;Constance Variable&quot;&gt;lambadalambda&lt;/a&gt; get #&lt;span class=&quot;tag&quot;&gt;&lt;a href=&quot;https://shitposter.club/tag/cofe&quot; rel=&quot;tag&quot;&gt;cofe&lt;/a&gt;&lt;/span&gt;</content>
+ <link rel="alternate" type="text/html" href="https://social.heldscal.la/notice/2013206"/>
+ <activity:verb>http://activitystrea.ms/schema/1.0/favorite</activity:verb>
+ <published>2017-05-02T12:22:03+00:00</published>
+ <updated>2017-05-02T12:22:03+00:00</updated>
+ <activity:object>
+  <activity:object-type>http://activitystrea.ms/schema/1.0/comment</activity:object-type>
+  <id>tag:shitposter.club,2017-05-02:noticeId=2783524:objectType=comment</id>
+  <title>New comment by shpuld</title>
+  <content type="html">@&lt;a href=&quot;https://social.heldscal.la/user/23211&quot; class=&quot;h-card mention&quot; title=&quot;Constance Variable&quot;&gt;lambadalambda&lt;/a&gt; get #&lt;span class=&quot;tag&quot;&gt;&lt;a href=&quot;https://shitposter.club/tag/cofe&quot; rel=&quot;tag&quot;&gt;cofe&lt;/a&gt;&lt;/span&gt;</content>
+  <link rel="alternate" type="text/html" href="https://shitposter.club/notice/2783524"/>
+  <status_net notice_id="2013199"></status_net>
+ </activity:object>
+ <thr:in-reply-to ref="tag:shitposter.club,2017-05-02:noticeId=2783524:objectType=comment" href="https://shitposter.club/notice/2783524"></thr:in-reply-to>
+ <link rel="related" href="https://shitposter.club/notice/2783524"/>
+ <link rel="ostatus:conversation" href="https://social.heldscal.la/conversation/1034065"/>
+ <ostatus:conversation href="https://social.heldscal.la/conversation/1034065" local_id="1034065" ref="tag:social.heldscal.la,2017-05-02:objectType=thread:nonce=2c27c27df8ec4dcc">tag:social.heldscal.la,2017-05-02:objectType=thread:nonce=2c27c27df8ec4dcc</ostatus:conversation>
+ <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
+ <link rel="self" type="application/atom+xml" href="https://social.heldscal.la/api/statuses/show/2013206.atom"/>
+ <link rel="edit" type="application/atom+xml" href="https://social.heldscal.la/api/statuses/show/2013206.atom"/>
+ <statusnet:notice_info local_id="2013206" source="unknown"></statusnet:notice_info>
+</entry>
+<entry>
+ <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
+ <id>tag:social.heldscal.la,2017-05-02:noticeId=2013185:objectType=note</id>
+ <title>New note by lambadalambda</title>
+ <content type="html">What now? &lt;a href=&quot;https://social.heldscal.la/file/e4822d95de677757ff50d49672a4046c83218b76c04a0ad5e5f1f0a9a9eb1a74.gif&quot; title=&quot;https://social.heldscal.la/file/e4822d95de677757ff50d49672a4046c83218b76c04a0ad5e5f1f0a9a9eb1a74.gif&quot; rel=&quot;nofollow external noreferrer&quot; class=&quot;attachment&quot; id=&quot;attachment-422572&quot;&gt;https://social.heldscal.la/attachment/422572&lt;/a&gt;</content>
+ <link rel="alternate" type="text/html" href="https://social.heldscal.la/notice/2013185"/>
+ <status_net notice_id="2013185"></status_net>
+ <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
+ <published>2017-05-02T12:21:04+00:00</published>
+ <updated>2017-05-02T12:21:04+00:00</updated>
+ <link rel="ostatus:conversation" href="https://social.heldscal.la/conversation/1034065"/>
+ <ostatus:conversation href="https://social.heldscal.la/conversation/1034065" local_id="1034065" ref="tag:social.heldscal.la,2017-05-02:objectType=thread:nonce=2c27c27df8ec4dcc">tag:social.heldscal.la,2017-05-02:objectType=thread:nonce=2c27c27df8ec4dcc</ostatus:conversation>
+ <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
+ <link rel="enclosure" href="https://social.heldscal.la/file/e4822d95de677757ff50d49672a4046c83218b76c04a0ad5e5f1f0a9a9eb1a74.gif" type="image/gif" length="132349"/>
+ <link rel="self" type="application/atom+xml" href="https://social.heldscal.la/api/statuses/show/2013185.atom"/>
+ <link rel="edit" type="application/atom+xml" href="https://social.heldscal.la/api/statuses/show/2013185.atom"/>
+ <statusnet:notice_info local_id="2013185" source="Pleroma FE"></statusnet:notice_info>
+</entry>
+<entry>
+ <id>tag:social.heldscal.la,2017-05-02:fave:23211:note:2012929:2017-05-02T12:01:25+00:00</id>
+ <title>Favorite</title>
+ <content type="html">lambadalambda favorited something by drkmttr: &lt;p&gt;&lt;span class=&quot;h-card&quot;&gt;&lt;a href=&quot;https://social.heldscal.la/lambadalambda&quot; class=&quot;u-url mention&quot;&gt;@&lt;span&gt;lambadalambda&lt;/span&gt;&lt;/a&gt;&lt;/span&gt; I checked out No Agenda because I saw you mention it several time. Sadly, I wasn't impressed. I'm all about varying perspectives but Adam and John basically just sound like resentful curmudgeons. It seems like their shtick is basically playing devil's advocate to everything to arouse some discontent. Just my two cents. 😉&lt;/p&gt;</content>
+ <link rel="alternate" type="text/html" href="https://social.heldscal.la/notice/2012940"/>
+ <activity:verb>http://activitystrea.ms/schema/1.0/favorite</activity:verb>
+ <published>2017-05-02T12:01:25+00:00</published>
+ <updated>2017-05-02T12:01:25+00:00</updated>
+ <activity:object>
+  <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
+  <id>tag:mstdn.io,2017-05-02:objectId=1310093:objectType=Status</id>
+  <title>New note by drkmttr</title>
+  <content type="html">&lt;p&gt;&lt;span class=&quot;h-card&quot;&gt;&lt;a href=&quot;https://social.heldscal.la/lambadalambda&quot; class=&quot;u-url mention&quot;&gt;@&lt;span&gt;lambadalambda&lt;/span&gt;&lt;/a&gt;&lt;/span&gt; I checked out No Agenda because I saw you mention it several time. Sadly, I wasn't impressed. I'm all about varying perspectives but Adam and John basically just sound like resentful curmudgeons. It seems like their shtick is basically playing devil's advocate to everything to arouse some discontent. Just my two cents. 😉&lt;/p&gt;</content>
+  <link rel="alternate" type="text/html" href="https://mstdn.io/users/drkmttr/updates/35653"/>
+  <status_net notice_id="2012929"></status_net>
+ </activity:object>
+ <thr:in-reply-to ref="tag:mstdn.io,2017-05-02:objectId=1310093:objectType=Status" href="https://mstdn.io/users/drkmttr/updates/35653"></thr:in-reply-to>
+ <link rel="related" href="https://mstdn.io/users/drkmttr/updates/35653"/>
+ <link rel="ostatus:conversation" href="https://social.heldscal.la/conversation/1033892"/>
+ <ostatus:conversation href="https://social.heldscal.la/conversation/1033892" local_id="1033892" ref="tag:social.heldscal.la,2017-05-02:objectType=thread:nonce=2f329b4eb20e83e2">tag:social.heldscal.la,2017-05-02:objectType=thread:nonce=2f329b4eb20e83e2</ostatus:conversation>
+ <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
+ <link rel="self" type="application/atom+xml" href="https://social.heldscal.la/api/statuses/show/2012940.atom"/>
+ <link rel="edit" type="application/atom+xml" href="https://social.heldscal.la/api/statuses/show/2012940.atom"/>
+ <statusnet:notice_info local_id="2012940" source="unknown"></statusnet:notice_info>
+</entry>
+<entry>
+ <id>tag:social.heldscal.la,2017-05-02:fave:23211:comment:2012336:2017-05-02T11:06:42+00:00</id>
+ <title>Favorite</title>
+ <content type="html">lambadalambda favorited something by clacke: @&lt;a href=&quot;https://mastodon.org.uk/users/dick_turpin&quot; class=&quot;h-card u-url p-nickname mention&quot; title=&quot;dick_turpin&quot;&gt;dickturpin&lt;/a&gt; @&lt;a href=&quot;http://quitter.se/user/113503&quot; class=&quot;h-card u-url p-nickname mention&quot; title=&quot;Luke&quot;&gt;luke&lt;/a&gt; Oh no, I miss being irritated by you, it  helps me understand myself and others. Also it builds character. :-)&lt;br /&gt; &lt;br /&gt; So if this is not federation because you can't follow all of online mankind, what should we call it? Proto-federated? Pre-federated?&lt;br /&gt; &lt;br /&gt; The term has been used decades ago for just one Microsoft Active Directory domain cross-certifying the root of another, by mutual agreement. I don't see how it's any less relevant to opportunistic federation between open servers on an open internet.&lt;br /&gt; &lt;br /&gt; I'm not saying we should be satisfied, I'm just saying that &quot;federate&quot; is a useful word and to build a big system we need to start with a small one. And focus on the things we *can* change, like helping the OStatus network grow and making the tools more useful.&lt;br /&gt; &lt;br /&gt; Saying that the network's ideals have failed because other networks aren't joining is doing neither of that.</content>
+ <link rel="alternate" type="text/html" href="https://social.heldscal.la/notice/2012341"/>
+ <activity:verb>http://activitystrea.ms/schema/1.0/favorite</activity:verb>
+ <published>2017-05-02T11:06:42+00:00</published>
+ <updated>2017-05-02T11:06:42+00:00</updated>
+ <activity:object>
+  <activity:object-type>http://activitystrea.ms/schema/1.0/comment</activity:object-type>
+  <id>tag:social.heldscal.la,2017-05-02:noticeId=2012336:objectType=comment</id>
+  <title>New comment by clacke</title>
+  <content type="html">@&lt;a href=&quot;https://mastodon.org.uk/users/dick_turpin&quot; class=&quot;h-card u-url p-nickname mention&quot; title=&quot;dick_turpin&quot;&gt;dickturpin&lt;/a&gt; @&lt;a href=&quot;http://quitter.se/user/113503&quot; class=&quot;h-card u-url p-nickname mention&quot; title=&quot;Luke&quot;&gt;luke&lt;/a&gt; Oh no, I miss being irritated by you, it  helps me understand myself and others. Also it builds character. :-)&lt;br /&gt; &lt;br /&gt; So if this is not federation because you can't follow all of online mankind, what should we call it? Proto-federated? Pre-federated?&lt;br /&gt; &lt;br /&gt; The term has been used decades ago for just one Microsoft Active Directory domain cross-certifying the root of another, by mutual agreement. I don't see how it's any less relevant to opportunistic federation between open servers on an open internet.&lt;br /&gt; &lt;br /&gt; I'm not saying we should be satisfied, I'm just saying that &amp;quot;federate&amp;quot; is a useful word and to build a big system we need to start with a small one. And focus on the things we *can* change, like helping the OStatus network grow and making the tools more useful.&lt;br /&gt; &lt;br /&gt; Saying that the network's ideals have failed because other networks aren't joining is doing neither of that.</content>
+  <link rel="alternate" type="text/html" href="https://social.heldscal.la/notice/2012336"/>
+  <status_net notice_id="2012336"></status_net>
+ </activity:object>
+ <thr:in-reply-to ref="tag:social.heldscal.la,2017-05-02:noticeId=2012336:objectType=comment" href="https://social.heldscal.la/notice/2012336"></thr:in-reply-to>
+ <link rel="related" href="https://social.heldscal.la/notice/2012336"/>
+ <link rel="ostatus:conversation" href="https://social.heldscal.la/conversation/1016421"/>
+ <ostatus:conversation href="https://social.heldscal.la/conversation/1016421" local_id="1016421" ref="https://s.wefamlee.be/conversation/16478">https://s.wefamlee.be/conversation/16478</ostatus:conversation>
+ <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
+ <link rel="self" type="application/atom+xml" href="https://social.heldscal.la/api/statuses/show/2012341.atom"/>
+ <link rel="edit" type="application/atom+xml" href="https://social.heldscal.la/api/statuses/show/2012341.atom"/>
+ <statusnet:notice_info local_id="2012341" source="unknown"></statusnet:notice_info>
+</entry>
+<entry>
+ <id>tag:social.heldscal.la,2017-05-02:fave:23211:comment:2011332:2017-05-02T10:37:40+00:00</id>
+ <title>Favorite</title>
+ <content type="html">lambadalambda favorited something by moonman: @&lt;a href=&quot;https://social.heldscal.la/user/23211&quot; class=&quot;h-card mention&quot; title=&quot;Constance Variable&quot;&gt;lambadalambda&lt;/a&gt; &lt;a href=&quot;https://www.youtube.com/watch?v=mKLizztikRk&quot; title=&quot;https://www.youtube.com/watch?v=mKLizztikRk&quot; class=&quot;attachment&quot; rel=&quot;nofollow&quot;&gt;https://www.youtube.com/watch?v=mKLizztikRk&lt;/a&gt;</content>
+ <link rel="alternate" type="text/html" href="https://social.heldscal.la/notice/2012148"/>
+ <activity:verb>http://activitystrea.ms/schema/1.0/favorite</activity:verb>
+ <published>2017-05-02T10:37:40+00:00</published>
+ <updated>2017-05-02T10:37:40+00:00</updated>
+ <activity:object>
+  <activity:object-type>http://activitystrea.ms/schema/1.0/comment</activity:object-type>
+  <id>tag:shitposter.club,2017-05-02:noticeId=2781833:objectType=comment</id>
+  <title>New comment by moonman</title>
+  <content type="html">@&lt;a href=&quot;https://social.heldscal.la/user/23211&quot; class=&quot;h-card mention&quot; title=&quot;Constance Variable&quot;&gt;lambadalambda&lt;/a&gt; &lt;a href=&quot;https://www.youtube.com/watch?v=mKLizztikRk&quot; title=&quot;https://www.youtube.com/watch?v=mKLizztikRk&quot; class=&quot;attachment&quot; rel=&quot;nofollow&quot;&gt;https://www.youtube.com/watch?v=mKLizztikRk&lt;/a&gt;</content>
+  <link rel="alternate" type="text/html" href="https://shitposter.club/notice/2781833"/>
+  <status_net notice_id="2011332"></status_net>
+ </activity:object>
+ <thr:in-reply-to ref="tag:shitposter.club,2017-05-02:noticeId=2781833:objectType=comment" href="https://shitposter.club/notice/2781833"></thr:in-reply-to>
+ <link rel="related" href="https://shitposter.club/notice/2781833"/>
+ <link rel="ostatus:conversation" href="https://social.heldscal.la/conversation/1032783"/>
+ <ostatus:conversation href="https://social.heldscal.la/conversation/1032783" local_id="1032783" ref="tag:social.heldscal.la,2017-05-02:objectType=thread:nonce=11d8b8c27d9513ec">tag:social.heldscal.la,2017-05-02:objectType=thread:nonce=11d8b8c27d9513ec</ostatus:conversation>
+ <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
+ <link rel="self" type="application/atom+xml" href="https://social.heldscal.la/api/statuses/show/2012148.atom"/>
+ <link rel="edit" type="application/atom+xml" href="https://social.heldscal.la/api/statuses/show/2012148.atom"/>
+ <statusnet:notice_info local_id="2012148" source="unknown"></statusnet:notice_info>
+</entry>
+<entry>
+ <activity:object-type>http://activitystrea.ms/schema/1.0/comment</activity:object-type>
+ <id>tag:social.heldscal.la,2017-05-02:noticeId=2012145:objectType=comment</id>
+ <title>New comment by lambadalambda</title>
+ <content type="html">@&lt;a href=&quot;https://sealion.club/user/186&quot; class=&quot;h-card u-url p-nickname mention&quot; title=&quot;I'M CEREAL U GUISE&quot;&gt;cereal&lt;/a&gt; ? No, you don't even need the identity servers for federation.</content>
+ <link rel="alternate" type="text/html" href="https://social.heldscal.la/notice/2012145"/>
+ <status_net notice_id="2012145"></status_net>
+ <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
+ <published>2017-05-02T10:37:33+00:00</published>
+ <updated>2017-05-02T10:37:33+00:00</updated>
+ <thr:in-reply-to ref="tag:sealion.club,2017-05-02:noticeId=3056001:objectType=comment" href="https://sealion.club/notice/3056001"></thr:in-reply-to>
+ <link rel="related" href="https://sealion.club/notice/3056001"/>
+ <link rel="ostatus:conversation" href="https://social.heldscal.la/conversation/1033277"/>
+ <ostatus:conversation href="https://social.heldscal.la/conversation/1033277" local_id="1033277" ref="https://sealion.club/conversation/1629037">https://sealion.club/conversation/1629037</ostatus:conversation>
+ <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="https://sealion.club/user/186"/>
+ <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
+ <link rel="self" type="application/atom+xml" href="https://social.heldscal.la/api/statuses/show/2012145.atom"/>
+ <link rel="edit" type="application/atom+xml" href="https://social.heldscal.la/api/statuses/show/2012145.atom"/>
+ <statusnet:notice_info local_id="2012145" source="Pleroma FE"></statusnet:notice_info>
+</entry>
+</feed>
diff --git a/test/fixtures/incoming_note_activity.xml b/test/fixtures/incoming_note_activity.xml
new file mode 100644 (file)
index 0000000..e54b25e
--- /dev/null
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8" ?><entry xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0" xmlns:activity="http://activitystrea.ms/spec/1.0/" xmlns:georss="http://www.georss.org/georss" xmlns:ostatus="http://ostatus.org/schema/1.0" xmlns:poco="http://portablecontacts.net/spec/1.0" xmlns:media="http://purl.org/syndication/atommedia" xmlns:statusnet="http://status.net/schema/api/1/">
+ <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
+ <id>tag:gs.example.org:4040,2017-04-23:noticeId=29:objectType=note</id>
+ <title>New note by lambda</title>
+ <content type="html">@&lt;a href=&quot;http://pleroma.example.org:4000/users/lain3&quot; class=&quot;h-card mention&quot;&gt;lain3&lt;/a&gt;</content>
+ <link rel="alternate" type="text/html" href="http://gs.example.org:4040/index.php/notice/29"/>
+ <status_net notice_id="29"></status_net>
+ <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
+ <published>2017-04-23T14:51:03+00:00</published>
+ <updated>2017-04-23T14:51:03+00:00</updated>
+ <author>
+  <activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
+  <uri>http://gs.example.org:4040/index.php/user/1</uri>
+  <name>lambda</name>
+  <link rel="alternate" type="text/html" href="http://gs.example.org:4040/index.php/lambda"/>
+  <link rel="avatar" type="image/png" media:width="96" media:height="96" href="http://gs.example.org:4040/theme/neo-gnu/default-avatar-profile.png"/>
+  <link rel="avatar" type="image/png" media:width="48" media:height="48" href="http://gs.example.org:4040/theme/neo-gnu/default-avatar-stream.png"/>
+  <link rel="avatar" type="image/png" media:width="24" media:height="24" href="http://gs.example.org:4040/theme/neo-gnu/default-avatar-mini.png"/>
+  <poco:preferredUsername>lambda</poco:preferredUsername>
+  <poco:displayName>lambda</poco:displayName>
+  <followers url="http://gs.example.org:4040/index.php/lambda/subscribers"></followers>
+  <statusnet:profile_info local_id="1"></statusnet:profile_info>
+ </author>
+ <link rel="ostatus:conversation" href="tag:gs.example.org:4040,2017-04-23:objectType=thread:nonce=f09e22f58abd5c7b"/>
+ <ostatus:conversation>tag:gs.example.org:4040,2017-04-23:objectType=thread:nonce=f09e22f58abd5c7b</ostatus:conversation>
+ <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="http://pleroma.example.org:4000/users/lain3"/>
+ <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
+ <source>
+  <id>http://gs.example.org:4040/index.php/api/statuses/user_timeline/1.atom</id>
+  <title>lambda</title>
+  <link rel="alternate" type="text/html" href="http://gs.example.org:4040/index.php/lambda"/>
+  <link rel="self" type="application/atom+xml" href="http://gs.example.org:4040/index.php/api/statuses/user_timeline/1.atom"/>
+  <link rel="license" href="https://creativecommons.org/licenses/by/3.0/"/>
+  <icon>http://gs.example.org:4040/theme/neo-gnu/default-avatar-profile.png</icon>
+  <updated>2017-04-23T14:51:03+00:00</updated>
+ </source>
+ <link rel="self" type="application/atom+xml" href="http://gs.example.org:4040/index.php/api/statuses/show/29.atom"/>
+ <link rel="edit" type="application/atom+xml" href="http://gs.example.org:4040/index.php/api/statuses/show/29.atom"/>
+ <statusnet:notice_info local_id="29" source="web"></statusnet:notice_info>
+</entry>
diff --git a/test/fixtures/incoming_note_activity_answer.xml b/test/fixtures/incoming_note_activity_answer.xml
new file mode 100644 (file)
index 0000000..b1244fa
--- /dev/null
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8" ?><entry xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0" xmlns:activity="http://activitystrea.ms/spec/1.0/" xmlns:georss="http://www.georss.org/georss" xmlns:ostatus="http://ostatus.org/schema/1.0" xmlns:poco="http://portablecontacts.net/spec/1.0" xmlns:media="http://purl.org/syndication/atommedia" xmlns:statusnet="http://status.net/schema/api/1/">
+ <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
+ <id>tag:gs.example.org:4040,2017-04-25:noticeId=55:objectType=note</id>
+ <title>New note by lambda</title>
+ <content type="html">hey.</content>
+ <link rel="alternate" type="text/html" href="http://gs.example.org:4040/index.php/notice/55"/>
+ <status_net notice_id="55"></status_net>
+ <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
+ <published>2017-04-25T18:16:13+00:00</published>
+ <updated>2017-04-25T18:16:13+00:00</updated>
+ <author>
+  <activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
+  <uri>http://gs.example.org:4040/index.php/user/1</uri>
+  <name>lambda</name>
+  <link rel="alternate" type="text/html" href="http://gs.example.org:4040/index.php/lambda"/>
+  <link rel="avatar" type="image/png" media:width="96" media:height="96" href="http://gs.example.org:4040/theme/neo-gnu/default-avatar-profile.png"/>
+  <link rel="avatar" type="image/png" media:width="48" media:height="48" href="http://gs.example.org:4040/theme/neo-gnu/default-avatar-stream.png"/>
+  <link rel="avatar" type="image/png" media:width="24" media:height="24" href="http://gs.example.org:4040/theme/neo-gnu/default-avatar-mini.png"/>
+  <poco:preferredUsername>lambda</poco:preferredUsername>
+  <poco:displayName>lambda</poco:displayName>
+  <followers url="http://gs.example.org:4040/index.php/lambda/subscribers"></followers>
+  <statusnet:profile_info local_id="1"></statusnet:profile_info>
+ </author>
+ <thr:in-reply-to ref="http://pleroma.example.org:4000/objects/55bce8fc-b423-46b1-af71-3759ab4670bc" href="http://pleroma.example.org:4000/objects/55bce8fc-b423-46b1-af71-3759ab4670bc"></thr:in-reply-to>
+ <link rel="related" href="http://pleroma.example.org:4000/objects/55bce8fc-b423-46b1-af71-3759ab4670bc"/>
+ <link rel="ostatus:conversation" href="http://pleroma.example.org:4000/contexts/8f6f45d4-8e4d-4e1a-a2de-09f27367d2d0"/>
+ <ostatus:conversation>http://pleroma.example.org:4000/contexts/8f6f45d4-8e4d-4e1a-a2de-09f27367d2d0</ostatus:conversation>
+ <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="http://pleroma.example.org:4000/users/lain5"/>
+ <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
+ <source>
+  <id>http://gs.example.org:4040/index.php/api/statuses/user_timeline/1.atom</id>
+  <title>lambda</title>
+  <link rel="alternate" type="text/html" href="http://gs.example.org:4040/index.php/lambda"/>
+  <link rel="self" type="application/atom+xml" href="http://gs.example.org:4040/index.php/api/statuses/user_timeline/1.atom"/>
+  <link rel="license" href="https://creativecommons.org/licenses/by/3.0/"/>
+  <icon>http://gs.example.org:4040/theme/neo-gnu/default-avatar-profile.png</icon>
+  <updated>2017-04-25T18:16:13+00:00</updated>
+ </source>
+ <link rel="self" type="application/atom+xml" href="http://gs.example.org:4040/index.php/api/statuses/show/55.atom"/>
+ <link rel="edit" type="application/atom+xml" href="http://gs.example.org:4040/index.php/api/statuses/show/55.atom"/>
+ <statusnet:notice_info local_id="55" source="web"></statusnet:notice_info>
+</entry>
diff --git a/test/fixtures/incoming_reply_mastodon.xml b/test/fixtures/incoming_reply_mastodon.xml
new file mode 100644 (file)
index 0000000..8ee1186
--- /dev/null
@@ -0,0 +1,29 @@
+<?xml version="1.0"?>
+<entry xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0" xmlns:activity="http://activitystrea.ms/spec/1.0/" xmlns:poco="http://portablecontacts.net/spec/1.0" xmlns:media="http://purl.org/syndication/atommedia" xmlns:ostatus="http://ostatus.org/schema/1.0" xmlns:mastodon="http://mastodon.social/schema/1.0">
+  <id>tag:mastodon.social,2017-05-02:objectId=4901603:objectType=Status</id>
+  <published>2017-05-02T18:33:06Z</published>
+  <updated>2017-05-02T18:33:06Z</updated>
+  <title>New status by lambadalambda</title>
+  <author>
+    <id>https://mastodon.social/users/lambadalambda</id>
+    <activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
+    <uri>https://mastodon.social/users/lambadalambda</uri>
+    <name>lambadalambda</name>
+    <email>lambadalambda@mastodon.social</email>
+    <link rel="alternate" type="text/html" href="https://mastodon.social/@lambadalambda"/>
+    <link rel="avatar" type="image/gif" media:width="120" media:height="120" href="https://files.mastodon.social/accounts/avatars/000/000/264/original/1429214160519.gif"/>
+    <link rel="header" type="" media:width="700" media:height="335" href="/headers/original/missing.png"/>
+    <poco:preferredUsername>lambadalambda</poco:preferredUsername>
+    <poco:displayName>Critical Value</poco:displayName>
+    <mastodon:scope>public</mastodon:scope>
+  </author>
+  <activity:object-type>http://activitystrea.ms/schema/1.0/comment</activity:object-type>
+  <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
+  <content type="html" xml:lang="el">&lt;p&gt;&lt;span class="h-card"&gt;&lt;a href="https://pleroma.soykaf.com/users/lain" class="u-url mention"&gt;@&lt;span&gt;lain&lt;/span&gt;&lt;/a&gt;&lt;/span&gt; hey&lt;/p&gt;</content>
+  <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="https://pleroma.soykaf.com/users/lain"/>
+  <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
+  <mastodon:scope>public</mastodon:scope>
+  <link rel="alternate" type="text/html" href="https://mastodon.social/users/lambadalambda/updates/2224923"/>
+  <link rel="self" type="application/atom+xml" href="https://mastodon.social/users/lambadalambda/updates/2224923.atom"/>
+  <thr:in-reply-to ref="https://pleroma.soykaf.com/objects/c237d966-ac75-4fe3-a87a-d89d71a3a7a4" href=""/>
+</entry>
diff --git a/test/fixtures/incoming_websub_gnusocial_attachments.xml b/test/fixtures/incoming_websub_gnusocial_attachments.xml
new file mode 100644 (file)
index 0000000..9d331ef
--- /dev/null
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<feed xml:lang="en-US" xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0" xmlns:georss="http://www.georss.org/georss" xmlns:activity="http://activitystrea.ms/spec/1.0/" xmlns:media="http://purl.org/syndication/atommedia" xmlns:poco="http://portablecontacts.net/spec/1.0" xmlns:ostatus="http://ostatus.org/schema/1.0" xmlns:statusnet="http://status.net/schema/api/1/">
+ <generator uri="https://gnu.io/social" version="1.0.2-dev">GNU social</generator>
+ <id>https://social.heldscal.la/api/statuses/user_timeline/23211.atom</id>
+ <title>lambadalambda timeline</title>
+ <subtitle>Updates from lambadalambda on social.heldscal.la!</subtitle>
+ <logo>https://social.heldscal.la/avatar/23211-96-20170416114255.jpeg</logo>
+ <updated>2017-05-02T20:29:35+00:00</updated>
+<author>
+ <activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
+ <uri>https://social.heldscal.la/user/23211</uri>
+ <name>lambadalambda</name>
+ <summary>Call me Deacon Blues.</summary>
+ <link rel="alternate" type="text/html" href="https://social.heldscal.la/lambadalambda"/>
+ <link rel="avatar" type="image/jpeg" media:width="236" media:height="236" href="https://social.heldscal.la/avatar/23211-original-20170416114255.jpeg"/>
+ <link rel="avatar" type="image/jpeg" media:width="96" media:height="96" href="https://social.heldscal.la/avatar/23211-96-20170416114255.jpeg"/>
+ <link rel="avatar" type="image/jpeg" media:width="48" media:height="48" href="https://social.heldscal.la/avatar/23211-48-20170416114255.jpeg"/>
+ <link rel="avatar" type="image/jpeg" media:width="24" media:height="24" href="https://social.heldscal.la/avatar/23211-24-20170416114257.jpeg"/>
+ <poco:preferredUsername>lambadalambda</poco:preferredUsername>
+ <poco:displayName>Constance Variable</poco:displayName>
+ <poco:note>Call me Deacon Blues.</poco:note>
+ <poco:address>
+  <poco:formatted>Berlin</poco:formatted>
+ </poco:address>
+ <poco:urls>
+  <poco:type>homepage</poco:type>
+  <poco:value>https://heldscal.la</poco:value>
+  <poco:primary>true</poco:primary>
+ </poco:urls>
+ <followers url="https://social.heldscal.la/lambadalambda/subscribers"></followers>
+ <statusnet:profile_info local_id="23211"></statusnet:profile_info>
+</author>
+ <link href="https://social.heldscal.la/lambadalambda" rel="alternate" type="text/html"/>
+ <link href="https://social.heldscal.la/main/sup" rel="http://api.friendfeed.com/2008/03#sup" type="application/json"/>
+ <link href="https://social.heldscal.la/main/push/hub" rel="hub"/>
+ <link href="https://social.heldscal.la/main/salmon/user/23211" rel="salmon"/>
+ <link href="https://social.heldscal.la/main/salmon/user/23211" rel="http://salmon-protocol.org/ns/salmon-replies"/>
+ <link href="https://social.heldscal.la/main/salmon/user/23211" rel="http://salmon-protocol.org/ns/salmon-mention"/>
+ <link href="https://social.heldscal.la/api/statuses/user_timeline/23211.atom" rel="self" type="application/atom+xml"/>
+<entry>
+ <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
+ <id>tag:social.heldscal.la,2017-05-02:noticeId=2020923:objectType=note</id>
+ <title>New note by lambadalambda</title>
+ <content type="html">Okay gonna stream some cool games!! &lt;a href=&quot;https://social.heldscal.la/file/7ed5ee508e6376a6e3dd581e17e7ed0b7b638147c7e86784bf83abc2641ee3d4.gif&quot; title=&quot;https://social.heldscal.la/file/7ed5ee508e6376a6e3dd581e17e7ed0b7b638147c7e86784bf83abc2641ee3d4.gif&quot; rel=&quot;nofollow external noreferrer&quot; class=&quot;attachment&quot; id=&quot;attachment-423842&quot;&gt;https://social.heldscal.la/attachment/423842&lt;/a&gt; &lt;a href=&quot;https://social.heldscal.la/file/4c209099cadfc5afd3e27a334aa0db96b3a7510dde1603305d68a2707e59a11f.png&quot; title=&quot;https://social.heldscal.la/file/4c209099cadfc5afd3e27a334aa0db96b3a7510dde1603305d68a2707e59a11f.png&quot; rel=&quot;nofollow external noreferrer&quot; class=&quot;attachment&quot; id=&quot;attachment-423843&quot;&gt;https://social.heldscal.la/attachment/423843&lt;/a&gt;</content>
+ <link rel="alternate" type="text/html" href="https://social.heldscal.la/notice/2020923"/>
+ <status_net notice_id="2020923"></status_net>
+ <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
+ <published>2017-05-02T20:29:35+00:00</published>
+ <updated>2017-05-02T20:29:35+00:00</updated>
+ <link rel="ostatus:conversation" href="https://social.heldscal.la/conversation/1038558"/>
+ <ostatus:conversation href="https://social.heldscal.la/conversation/1038558" local_id="1038558" ref="tag:social.heldscal.la,2017-05-02:objectType=thread:nonce=26c7afdcbcf4ebd4">tag:social.heldscal.la,2017-05-02:objectType=thread:nonce=26c7afdcbcf4ebd4</ostatus:conversation>
+ <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
+ <link rel="enclosure" href="https://social.heldscal.la/file/7ed5ee508e6376a6e3dd581e17e7ed0b7b638147c7e86784bf83abc2641ee3d4.gif" type="image/gif" length="17283"/>
+ <link rel="enclosure" href="https://social.heldscal.la/file/4c209099cadfc5afd3e27a334aa0db96b3a7510dde1603305d68a2707e59a11f.png" type="image/png" length="6965"/>
+ <link rel="self" type="application/atom+xml" href="https://social.heldscal.la/api/statuses/show/2020923.atom"/>
+ <link rel="edit" type="application/atom+xml" href="https://social.heldscal.la/api/statuses/show/2020923.atom"/>
+ <statusnet:notice_info local_id="2020923" source="Pleroma FE"></statusnet:notice_info>
+</entry>
+</feed>
diff --git a/test/fixtures/lambadalambda.atom b/test/fixtures/lambadalambda.atom
new file mode 100644 (file)
index 0000000..35e5064
--- /dev/null
@@ -0,0 +1,479 @@
+<?xml version="1.0"?>
+<feed xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0" xmlns:activity="http://activitystrea.ms/spec/1.0/" xmlns:poco="http://portablecontacts.net/spec/1.0" xmlns:media="http://purl.org/syndication/atommedia" xmlns:ostatus="http://ostatus.org/schema/1.0" xmlns:mastodon="http://mastodon.social/schema/1.0">
+  <id>https://mastodon.social/users/lambadalambda.atom</id>
+  <title>Critical Value</title>
+  <subtitle></subtitle>
+  <updated>2017-04-16T21:47:25Z</updated>
+  <logo>https://files.mastodon.social/accounts/avatars/000/000/264/original/1429214160519.gif?1492379244</logo>
+  <author>
+    <id>https://mastodon.social/users/lambadalambda</id>
+    <activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
+    <uri>https://mastodon.social/users/lambadalambda</uri>
+    <name>lambadalambda</name>
+    <email>lambadalambda@mastodon.social</email>
+    <summary></summary>
+    <link rel="alternate" type="text/html" href="https://mastodon.social/@lambadalambda"/>
+    <link rel="avatar" type="image/gif" media:width="120" media:height="120" href="https://files.mastodon.social/accounts/avatars/000/000/264/original/1429214160519.gif?1492379244"/>
+    <link rel="header" type="" media:width="700" media:height="335" href="/headers/original/missing.png"/>
+    <poco:preferredUsername>lambadalambda</poco:preferredUsername>
+    <poco:displayName>Critical Value</poco:displayName>
+    <mastodon:scope>public</mastodon:scope>
+  </author>
+  <link rel="alternate" type="text/html" href="https://mastodon.social/@lambadalambda"/>
+  <link rel="self" type="application/atom+xml" href="https://mastodon.social/users/lambadalambda.atom"/>
+  <link rel="next" type="application/atom+xml" href="https://mastodon.social/users/lambadalambda.atom?max_id=1488609"/>
+  <link rel="hub" href="https://mastodon.social/api/push"/>
+  <link rel="salmon" href="https://mastodon.social/api/salmon/264"/>
+  <entry>
+    <id>tag:mastodon.social,2017-04-07:objectId=1874242:objectType=Status</id>
+    <published>2017-04-07T11:02:56Z</published>
+    <updated>2017-04-07T11:02:56Z</updated>
+    <title>lambadalambda shared a status by 0xroy@social.wxcafe.net</title>
+    <activity:object-type>http://activitystrea.ms/schema/1.0/activity</activity:object-type>
+    <activity:verb>http://activitystrea.ms/schema/1.0/share</activity:verb>
+    <activity:object>
+      <id>tag:social.wxcafe.net,2017-04-07:objectId=72554:objectType=Status</id>
+      <published>2017-04-07T11:01:59Z</published>
+      <updated>2017-04-07T11:02:00Z</updated>
+      <title>New status by 0xroy@social.wxcafe.net</title>
+      <author>
+        <id>https://social.wxcafe.net/users/0xroy</id>
+        <activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
+        <uri>https://social.wxcafe.net/users/0xroy</uri>
+        <name>0xroy</name>
+        <email>0xroy@social.wxcafe.net</email>
+        <summary>ta caution weeb | discussions privées : &lt;a href="https://💌.0xroy.me" rel="nofollow noopener" target="_blank"&gt;&lt;span class="invisible"&gt;https://&lt;/span&gt;&lt;span class=""&gt;💌.0xroy.me&lt;/span&gt;&lt;span class="invisible"&gt;&lt;/span&gt;&lt;/a&gt;</summary>
+        <link rel="alternate" type="text/html" href="https://social.wxcafe.net/@0xroy"/>
+        <link rel="avatar" type="image/jpeg" media:width="120" media:height="120" href="https://files.mastodon.social/accounts/avatars/000/036/953/original/20068e41d0310172.jpg?1491240516"/>
+        <link rel="header" type="image/jpeg" media:width="700" media:height="335" href="https://files.mastodon.social/accounts/headers/000/036/953/original/2229d0e3f129fe8c.jpg?1491381114"/>
+        <poco:preferredUsername>0xroy</poco:preferredUsername>
+        <poco:displayName>「R O Y  🍵 B O S」</poco:displayName>
+        <poco:note>ta caution weeb | discussions privées : &lt;a href="https://%F0%9F%92%8C.0xroy.me" rel="nofollow noopener"&gt;&lt;span class="invisible"&gt;https://&lt;/span&gt;&lt;span class=""&gt;💌.0xroy.me&lt;/span&gt;&lt;span class="invisible"&gt;&lt;/span&gt;&lt;/a&gt;</poco:note>
+        <mastodon:scope>public</mastodon:scope>
+      </author>
+      <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
+      <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
+      <content type="html" xml:lang="en">&lt;p&gt;someone pls eli5 matrix (protocol) and riot&lt;/p&gt;</content>
+      <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
+      <mastodon:scope>public</mastodon:scope>
+      <link rel="alternate" type="text/html" href="https://social.wxcafe.net/users/0xroy/updates/4510"/>
+    </activity:object>
+    <content type="html" xml:lang="en">&lt;p&gt;someone pls eli5 matrix (protocol) and riot&lt;/p&gt;</content>
+    <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
+    <mastodon:scope>public</mastodon:scope>
+    <link rel="alternate" type="text/html" href="https://mastodon.social/users/lambadalambda/updates/1689208"/>
+    <link rel="self" type="application/atom+xml" href="https://mastodon.social/users/lambadalambda/updates/1689208.atom"/>
+  </entry>
+  <entry>
+    <id>tag:mastodon.social,2017-04-06:objectId=1768247:objectType=Status</id>
+    <published>2017-04-06T11:10:19Z</published>
+    <updated>2017-04-06T11:10:19Z</updated>
+    <title>lambadalambda shared a status by areyoutoo@mastodon.xyz</title>
+    <activity:object-type>http://activitystrea.ms/schema/1.0/activity</activity:object-type>
+    <activity:verb>http://activitystrea.ms/schema/1.0/share</activity:verb>
+    <activity:object>
+      <id>tag:mastodon.xyz,2017-04-05:objectId=133327:objectType=Status</id>
+      <published>2017-04-05T17:36:41Z</published>
+      <updated>2017-04-05T18:12:14Z</updated>
+      <title>New status by areyoutoo@mastodon.xyz</title>
+      <author>
+        <id>https://mastodon.xyz/users/areyoutoo</id>
+        <activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
+        <uri>https://mastodon.xyz/users/areyoutoo</uri>
+        <name>areyoutoo</name>
+        <email>areyoutoo@mastodon.xyz</email>
+        <summary>devops | retired gamedev | always boost puppy pics</summary>
+        <link rel="alternate" type="text/html" href="https://mastodon.xyz/@areyoutoo"/>
+        <link rel="avatar" type="image/png" media:width="120" media:height="120" href="https://files.mastodon.social/accounts/avatars/000/047/888/original/5ce2e132d4c18d65.png?1491343828"/>
+        <link rel="header" type="image/png" media:width="700" media:height="335" href="https://files.mastodon.social/accounts/headers/000/047/888/original/missing.png?1491336769"/>
+        <poco:preferredUsername>areyoutoo</poco:preferredUsername>
+        <poco:displayName>Raw Butter</poco:displayName>
+        <poco:note>devops | retired gamedev | always boost puppy pics</poco:note>
+        <mastodon:scope>public</mastodon:scope>
+      </author>
+      <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
+      <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
+      <content type="html" xml:lang="en">&lt;p&gt;Some UX thoughts for &lt;a href="https://mastodon.xyz/tags/mastodev" class="mention hashtag"&gt;#&lt;span&gt;mastodev&lt;/span&gt;&lt;/a&gt;:&lt;/p&gt;&lt;p&gt;- Would be nice if I could work on multiple draft toots? Clicking to reply to someone seems to erase any draft I had been working on.&lt;/p&gt;&lt;p&gt;- Kinda risky to click on the Federated Timeline if it loads new toots and scrolls 10ms before I click on something.&lt;/p&gt;&lt;p&gt;I probably don't know enough web frontend to help, but it might be fun to try.&lt;/p&gt;</content>
+      <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
+      <category term="mastodev"/>
+      <mastodon:scope>public</mastodon:scope>
+      <link rel="alternate" type="text/html" href="https://mastodon.xyz/users/areyoutoo/updates/36028"/>
+    </activity:object>
+    <content type="html" xml:lang="en">&lt;p&gt;Some UX thoughts for &lt;a href="https://mastodon.xyz/tags/mastodev" class="mention hashtag"&gt;#&lt;span&gt;mastodev&lt;/span&gt;&lt;/a&gt;:&lt;/p&gt;&lt;p&gt;- Would be nice if I could work on multiple draft toots? Clicking to reply to someone seems to erase any draft I had been working on.&lt;/p&gt;&lt;p&gt;- Kinda risky to click on the Federated Timeline if it loads new toots and scrolls 10ms before I click on something.&lt;/p&gt;&lt;p&gt;I probably don't know enough web frontend to help, but it might be fun to try.&lt;/p&gt;</content>
+    <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
+    <mastodon:scope>public</mastodon:scope>
+    <link rel="alternate" type="text/html" href="https://mastodon.social/users/lambadalambda/updates/1658950"/>
+    <link rel="self" type="application/atom+xml" href="https://mastodon.social/users/lambadalambda/updates/1658950.atom"/>
+  </entry>
+  <entry>
+    <id>tag:mastodon.social,2017-04-06:objectId=1764509:objectType=Status</id>
+    <published>2017-04-06T10:15:38Z</published>
+    <updated>2017-04-06T10:15:38Z</updated>
+    <title>New status by lambadalambda</title>
+    <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
+    <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
+    <summary xml:lang="en">This is a test for cw federation</summary>
+    <content type="html" xml:lang="en">&lt;p&gt;This is a test for cw federation body text.&lt;/p&gt;</content>
+    <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
+    <mastodon:scope>public</mastodon:scope>
+    <link rel="alternate" type="text/html" href="https://mastodon.social/users/lambadalambda/updates/1657819"/>
+    <link rel="self" type="application/atom+xml" href="https://mastodon.social/users/lambadalambda/updates/1657819.atom"/>
+  </entry>
+  <entry>
+    <id>tag:mastodon.social,2017-04-05:objectId=1645208:objectType=Status</id>
+    <published>2017-04-05T07:14:53Z</published>
+    <updated>2017-04-05T07:14:53Z</updated>
+    <title>lambadalambda shared a status by lambadalambda@social.heldscal.la</title>
+    <activity:object-type>http://activitystrea.ms/schema/1.0/activity</activity:object-type>
+    <activity:verb>http://activitystrea.ms/schema/1.0/share</activity:verb>
+    <activity:object>
+      <id>tag:social.heldscal.la,2017-04-05:noticeId=1502088:objectType=note</id>
+      <published>2017-04-05T06:12:09Z</published>
+      <updated>2017-04-05T07:12:47Z</updated>
+      <title>New status by lambadalambda@social.heldscal.la</title>
+      <author>
+        <id>https://social.heldscal.la/user/23211</id>
+        <activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
+        <uri>https://social.heldscal.la/user/23211</uri>
+        <name>lambadalambda</name>
+        <email>lambadalambda@social.heldscal.la</email>
+        <summary>Call me Deacon Blues.</summary>
+        <link rel="alternate" type="text/html" href="https://social.heldscal.la/lambadalambda"/>
+        <link rel="avatar" type="image/jpeg" media:width="120" media:height="120" href="https://files.mastodon.social/accounts/avatars/000/000/236/original/23211-original-20170416114255.jpeg?1492345317"/>
+        <link rel="header" type="" media:width="700" media:height="335" href="/headers/original/missing.png"/>
+        <poco:preferredUsername>lambadalambda</poco:preferredUsername>
+        <poco:displayName>Constance Variable</poco:displayName>
+        <poco:note>Call me Deacon Blues.</poco:note>
+        <mastodon:scope>public</mastodon:scope>
+      </author>
+      <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
+      <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
+      <content type="html" xml:lang="en">Federation 101: &lt;a href="https://www.youtube.com/watch?v=t1lYU5CA40o" rel="nofollow external noreferrer" class="attachment thumbnail"&gt;https://www.youtube.com/watch?v=t1lYU5CA40o&lt;/a&gt;</content>
+      <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
+      <mastodon:scope>public</mastodon:scope>
+      <link rel="alternate" type="text/html" href="https://social.heldscal.la/notice/1502088"/>
+    </activity:object>
+    <content type="html" xml:lang="en">Federation 101: &lt;a href="https://www.youtube.com/watch?v=t1lYU5CA40o" rel="nofollow external noreferrer" class="attachment thumbnail"&gt;https://www.youtube.com/watch?v=t1lYU5CA40o&lt;/a&gt;</content>
+    <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
+    <mastodon:scope>public</mastodon:scope>
+    <link rel="alternate" type="text/html" href="https://mastodon.social/users/lambadalambda/updates/1618003"/>
+    <link rel="self" type="application/atom+xml" href="https://mastodon.social/users/lambadalambda/updates/1618003.atom"/>
+  </entry>
+  <entry>
+    <id>tag:mastodon.social,2017-04-05:objectId=1641750:objectType=Status</id>
+    <published>2017-04-05T05:44:48Z</published>
+    <updated>2017-04-05T05:44:48Z</updated>
+    <title>New status by lambadalambda</title>
+    <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
+    <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
+    <content type="html" xml:lang="en">&lt;p&gt;&lt;span class="h-card"&gt;&lt;a href="https://social.heldscal.la/lambadalambda" class="u-url mention"&gt;@&lt;span&gt;lambadalambda&lt;/span&gt;&lt;/a&gt;&lt;/span&gt; just a test.&lt;/p&gt;</content>
+    <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="https://social.heldscal.la/user/23211"/>
+    <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
+    <mastodon:scope>public</mastodon:scope>
+    <link rel="alternate" type="text/html" href="https://mastodon.social/users/lambadalambda/updates/1616358"/>
+    <link rel="self" type="application/atom+xml" href="https://mastodon.social/users/lambadalambda/updates/1616358.atom"/>
+  </entry>
+  <entry>
+    <id>tag:mastodon.social,2017-04-04:objectId=1540149:objectType=Status</id>
+    <published>2017-04-04T06:31:09Z</published>
+    <updated>2017-04-04T06:31:09Z</updated>
+    <title>New status by lambadalambda</title>
+    <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
+    <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
+    <content type="html" xml:lang="en">&lt;p&gt;Looks like you still can&amp;apos;t delete your account here (PRIVACY!), but I won&amp;apos;t be posting here anymore, my main account is &lt;span class="h-card"&gt;&lt;a href="https://social.heldscal.la/lambadalambda" class="u-url mention"&gt;@&lt;span&gt;lambadalambda&lt;/span&gt;&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;</content>
+    <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="https://social.heldscal.la/user/23211"/>
+    <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
+    <mastodon:scope>public</mastodon:scope>
+    <link rel="alternate" type="text/html" href="https://mastodon.social/users/lambadalambda/updates/1559641"/>
+    <link rel="self" type="application/atom+xml" href="https://mastodon.social/users/lambadalambda/updates/1559641.atom"/>
+  </entry>
+  <entry>
+    <id>tag:mastodon.social,2017-04-04:objectId=1539608:objectType=Status</id>
+    <published>2017-04-04T06:18:16Z</published>
+    <updated>2017-04-04T06:18:16Z</updated>
+    <title>New status by lambadalambda</title>
+    <activity:object-type>http://activitystrea.ms/schema/1.0/comment</activity:object-type>
+    <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
+    <content type="html" xml:lang="en">&lt;p&gt;&lt;span class="h-card"&gt;&lt;a href="https://mastodon.social/@ghostbar" class="u-url mention"&gt;@&lt;span&gt;ghostbar&lt;/span&gt;&lt;/a&gt;&lt;/span&gt; Remember to rewrite it in Rust once you&amp;apos;re done.&lt;/p&gt;</content>
+    <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="https://mastodon.social/users/ghostbar"/>
+    <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
+    <mastodon:scope>public</mastodon:scope>
+    <link rel="alternate" type="text/html" href="https://mastodon.social/users/lambadalambda/updates/1559263"/>
+    <link rel="self" type="application/atom+xml" href="https://mastodon.social/users/lambadalambda/updates/1559263.atom"/>
+    <thr:in-reply-to ref="tag:mastodon.social,2017-04-03:objectId=1514426:objectType=Status" href="https://mastodon.social/@ghostbar/1514426"/>
+  </entry>
+  <entry>
+    <id>tag:mastodon.social,2017-04-03:objectId=1504813:objectType=Status</id>
+    <published>2017-04-03T18:01:20Z</published>
+    <updated>2017-04-03T18:01:20Z</updated>
+    <title>New status by lambadalambda</title>
+    <activity:object-type>http://activitystrea.ms/schema/1.0/comment</activity:object-type>
+    <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
+    <content type="html" xml:lang="en">&lt;p&gt;&lt;span class="h-card"&gt;&lt;a href="https://mastodon.xyz/@Azurolu" class="u-url mention"&gt;@&lt;span&gt;Azurolu&lt;/span&gt;&lt;/a&gt;&lt;/span&gt; You mean gs.smuglo.li?&lt;/p&gt;</content>
+    <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="https://mastodon.xyz/users/Azurolu"/>
+    <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
+    <mastodon:scope>public</mastodon:scope>
+    <link rel="alternate" type="text/html" href="https://mastodon.social/users/lambadalambda/updates/1535844"/>
+    <link rel="self" type="application/atom+xml" href="https://mastodon.social/users/lambadalambda/updates/1535844.atom"/>
+    <thr:in-reply-to ref="tag:mastodon.xyz,2017-04-03:objectId=21879:objectType=Status" href="https://mastodon.xyz/users/Azurolu/updates/3813"/>
+  </entry>
+  <entry>
+    <id>tag:mastodon.social,2017-04-03:objectId=1504805:objectType=Status</id>
+    <published>2017-04-03T18:01:05Z</published>
+    <updated>2017-04-03T18:01:05Z</updated>
+    <title>New status by lambadalambda</title>
+    <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
+    <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
+    <content type="html" xml:lang="en">&lt;p&gt;There&amp;apos;s nothing wrong with having several alt accounts all across the fediverse. Try out another mastodon instance (&lt;a href="https://icosahedron.website" rel="nofollow noopener" target="_blank"&gt;&lt;span class="invisible"&gt;https://&lt;/span&gt;&lt;span class=""&gt;icosahedron.website&lt;/span&gt;&lt;span class="invisible"&gt;&lt;/span&gt;&lt;/a&gt;) or a GNU Social instance (like &lt;a href="https://shitposter.club" rel="nofollow noopener" target="_blank"&gt;&lt;span class="invisible"&gt;https://&lt;/span&gt;&lt;span class=""&gt;shitposter.club&lt;/span&gt;&lt;span class="invisible"&gt;&lt;/span&gt;&lt;/a&gt; or &lt;a href="https://freezepeach.xyz" rel="nofollow noopener" target="_blank"&gt;&lt;span class="invisible"&gt;https://&lt;/span&gt;&lt;span class=""&gt;freezepeach.xyz&lt;/span&gt;&lt;span class="invisible"&gt;&lt;/span&gt;&lt;/a&gt;), or friendica. They are all on the same network, so you can still follow all your friends!&lt;/p&gt;</content>
+    <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
+    <mastodon:scope>public</mastodon:scope>
+    <link rel="alternate" type="text/html" href="https://mastodon.social/users/lambadalambda/updates/1535837"/>
+    <link rel="self" type="application/atom+xml" href="https://mastodon.social/users/lambadalambda/updates/1535837.atom"/>
+  </entry>
+  <entry>
+    <id>tag:mastodon.social,2017-04-03:objectId=1503965:objectType=Status</id>
+    <published>2017-04-03T17:31:30Z</published>
+    <updated>2017-04-03T17:31:30Z</updated>
+    <title>New status by lambadalambda</title>
+    <activity:object-type>http://activitystrea.ms/schema/1.0/comment</activity:object-type>
+    <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
+    <content type="html" xml:lang="en">&lt;p&gt;&lt;span class="h-card"&gt;&lt;a href="https://mastodon.social/@20Hz" class="u-url mention"&gt;@&lt;span&gt;20Hz&lt;/span&gt;&lt;/a&gt;&lt;/span&gt; you could also try out a GS instance, which are on the same network :)&lt;/p&gt;</content>
+    <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="https://mastodon.social/users/20Hz"/>
+    <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
+    <mastodon:scope>public</mastodon:scope>
+    <link rel="alternate" type="text/html" href="https://mastodon.social/users/lambadalambda/updates/1535176"/>
+    <link rel="self" type="application/atom+xml" href="https://mastodon.social/users/lambadalambda/updates/1535176.atom"/>
+    <thr:in-reply-to ref="tag:mastodon.social,2017-04-03:objectId=1503524:objectType=Status" href="https://mastodon.social/@20Hz/1503524"/>
+  </entry>
+  <entry>
+    <id>tag:mastodon.social,2017-04-03:objectId=1503955:objectType=Status</id>
+    <published>2017-04-03T17:31:08Z</published>
+    <updated>2017-04-03T17:31:08Z</updated>
+    <title>lambadalambda shared a status by shpuld@shitposter.club</title>
+    <activity:object-type>http://activitystrea.ms/schema/1.0/activity</activity:object-type>
+    <activity:verb>http://activitystrea.ms/schema/1.0/share</activity:verb>
+    <activity:object>
+      <id>tag:shitposter.club,2017-04-03:noticeId=2251717:objectType=note</id>
+      <published>2017-04-03T17:06:43Z</published>
+      <updated>2017-04-03T17:12:06Z</updated>
+      <title>New status by shpuld@shitposter.club</title>
+      <author>
+        <id>https://shitposter.club/user/5381</id>
+        <activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
+        <uri>https://shitposter.club/user/5381</uri>
+        <name>shpuld</name>
+        <email>shpuld@shitposter.club</email>
+        <summary></summary>
+        <link rel="alternate" type="text/html" href="https://shitposter.club/shpuld"/>
+        <link rel="avatar" type="image/jpeg" media:width="120" media:height="120" href="https://files.mastodon.social/accounts/avatars/000/005/895/original/5381-original-20170401213417.jpeg?1491082522"/>
+        <link rel="header" type="" media:width="700" media:height="335" href="/headers/original/missing.png"/>
+        <poco:preferredUsername>shpuld</poco:preferredUsername>
+        <poco:displayName>shp</poco:displayName>
+        <mastodon:scope>public</mastodon:scope>
+      </author>
+      <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
+      <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
+      <content type="html" xml:lang="en">reposting the classic &lt;a href="https://shitposter.club/file/89c5fe483526caf3a46cfc5cdd4ae68061054350e767397731af658d54786e31.jpg" class="attachment" rel="nofollow external"&gt;https://shitposter.club/attachment/219846&lt;/a&gt;</content>
+      <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
+      <link rel="enclosure" type="image/jpeg" length="30588" href="https://files.mastodon.social/media_attachments/files/000/156/256/original/89c5fe483526caf3a46cfc5cdd4ae68061054350e767397731af658d54786e31.jpg"/>
+      <mastodon:scope>public</mastodon:scope>
+      <link rel="alternate" type="text/html" href="https://shitposter.club/notice/2251717"/>
+    </activity:object>
+    <content type="html" xml:lang="en">reposting the classic &lt;a href="https://shitposter.club/file/89c5fe483526caf3a46cfc5cdd4ae68061054350e767397731af658d54786e31.jpg" class="attachment" rel="nofollow external"&gt;https://shitposter.club/attachment/219846&lt;/a&gt;</content>
+    <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
+    <mastodon:scope>public</mastodon:scope>
+    <link rel="alternate" type="text/html" href="https://mastodon.social/users/lambadalambda/updates/1535166"/>
+    <link rel="self" type="application/atom+xml" href="https://mastodon.social/users/lambadalambda/updates/1535166.atom"/>
+  </entry>
+  <entry>
+    <id>tag:mastodon.social,2017-04-03:objectId=1503929:objectType=Status</id>
+    <published>2017-04-03T17:30:43Z</published>
+    <updated>2017-04-03T17:30:43Z</updated>
+    <title>New status by lambadalambda</title>
+    <activity:object-type>http://activitystrea.ms/schema/1.0/comment</activity:object-type>
+    <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
+    <content type="html" xml:lang="en">&lt;p&gt;&lt;span class="h-card"&gt;&lt;a href="https://mastodon.social/@ghostbar" class="u-url mention"&gt;@&lt;span&gt;ghostbar&lt;/span&gt;&lt;/a&gt;&lt;/span&gt; Normally you shouldn&amp;apos;t be running tens of thousands of users on one instance... That&amp;apos;s one of the reasons for federation.&lt;/p&gt;</content>
+    <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="https://mastodon.social/users/ghostbar"/>
+    <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
+    <mastodon:scope>public</mastodon:scope>
+    <link rel="alternate" type="text/html" href="https://mastodon.social/users/lambadalambda/updates/1535144"/>
+    <link rel="self" type="application/atom+xml" href="https://mastodon.social/users/lambadalambda/updates/1535144.atom"/>
+    <thr:in-reply-to ref="tag:mastodon.social,2017-04-03:objectId=1503526:objectType=Status" href="https://mastodon.social/@ghostbar/1503526"/>
+  </entry>
+  <entry>
+    <id>tag:mastodon.social,2017-04-03:objectId=1477255:objectType=Status</id>
+    <published>2017-04-03T08:24:39Z</published>
+    <updated>2017-04-03T08:24:39Z</updated>
+    <title>New status by lambadalambda</title>
+    <activity:object-type>http://activitystrea.ms/schema/1.0/comment</activity:object-type>
+    <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
+    <content type="html" xml:lang="en">&lt;p&gt;&lt;span class="h-card"&gt;&lt;a href="https://mastodon.social/@dot_tiff" class="u-url mention"&gt;@&lt;span&gt;dot_tiff&lt;/span&gt;&lt;/a&gt;&lt;/span&gt; it&amp;apos;s the vaporwave mode.&lt;/p&gt;</content>
+    <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="https://mastodon.social/users/dot_tiff"/>
+    <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
+    <mastodon:scope>public</mastodon:scope>
+    <link rel="alternate" type="text/html" href="https://mastodon.social/users/lambadalambda/updates/1513305"/>
+    <link rel="self" type="application/atom+xml" href="https://mastodon.social/users/lambadalambda/updates/1513305.atom"/>
+    <thr:in-reply-to ref="tag:mastodon.social,2017-04-03:objectId=1477220:objectType=Status" href="https://mastodon.social/@dot_tiff/1477220"/>
+  </entry>
+  <entry>
+    <id>tag:mastodon.social,2017-04-03:objectId=1476210:objectType=Status</id>
+    <published>2017-04-03T07:45:42Z</published>
+    <updated>2017-04-03T07:45:42Z</updated>
+    <title>lambadalambda shared a status by lambadalambda@social.heldscal.la</title>
+    <activity:object-type>http://activitystrea.ms/schema/1.0/activity</activity:object-type>
+    <activity:verb>http://activitystrea.ms/schema/1.0/share</activity:verb>
+    <activity:object>
+      <id>tag:social.heldscal.la,2017-04-03:noticeId=1475727:objectType=note</id>
+      <published>2017-04-03T07:44:43Z</published>
+      <updated>2017-04-03T07:44:48Z</updated>
+      <title>New status by lambadalambda@social.heldscal.la</title>
+      <author>
+        <id>https://social.heldscal.la/user/23211</id>
+        <activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
+        <uri>https://social.heldscal.la/user/23211</uri>
+        <name>lambadalambda</name>
+        <email>lambadalambda@social.heldscal.la</email>
+        <summary>Call me Deacon Blues.</summary>
+        <link rel="alternate" type="text/html" href="https://social.heldscal.la/lambadalambda"/>
+        <link rel="avatar" type="image/jpeg" media:width="120" media:height="120" href="https://files.mastodon.social/accounts/avatars/000/000/236/original/23211-original-20170416114255.jpeg?1492345317"/>
+        <link rel="header" type="" media:width="700" media:height="335" href="/headers/original/missing.png"/>
+        <poco:preferredUsername>lambadalambda</poco:preferredUsername>
+        <poco:displayName>Constance Variable</poco:displayName>
+        <poco:note>Call me Deacon Blues.</poco:note>
+        <mastodon:scope>public</mastodon:scope>
+      </author>
+      <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
+      <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
+      <content type="html" xml:lang="en">Here's a song by the original anti-idol, Togawa Jun: &lt;a href="https://www.youtube.com/watch?v=kNI_NK2YY-s" rel="nofollow external noreferrer" class="attachment"&gt;https://www.youtube.com/watch?v=kNI_NK2YY-s&lt;/a&gt;</content>
+      <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
+      <mastodon:scope>public</mastodon:scope>
+      <link rel="alternate" type="text/html" href="https://social.heldscal.la/notice/1475727"/>
+    </activity:object>
+    <content type="html" xml:lang="en">Here's a song by the original anti-idol, Togawa Jun: &lt;a href="https://www.youtube.com/watch?v=kNI_NK2YY-s" rel="nofollow external noreferrer" class="attachment"&gt;https://www.youtube.com/watch?v=kNI_NK2YY-s&lt;/a&gt;</content>
+    <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
+    <mastodon:scope>public</mastodon:scope>
+    <link rel="alternate" type="text/html" href="https://mastodon.social/users/lambadalambda/updates/1512485"/>
+    <link rel="self" type="application/atom+xml" href="https://mastodon.social/users/lambadalambda/updates/1512485.atom"/>
+  </entry>
+  <entry>
+    <id>tag:mastodon.social,2017-04-03:objectId=1476047:objectType=Status</id>
+    <published>2017-04-03T07:39:14Z</published>
+    <updated>2017-04-03T07:39:14Z</updated>
+    <title>New status by lambadalambda</title>
+    <activity:object-type>http://activitystrea.ms/schema/1.0/comment</activity:object-type>
+    <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
+    <content type="html" xml:lang="en">&lt;p&gt;&lt;span class="h-card"&gt;&lt;a href="https://mastodon.social/@amrrr" class="u-url mention"&gt;@&lt;span&gt;amrrr&lt;/span&gt;&lt;/a&gt;&lt;/span&gt; tumblr/10, but pretty good!&lt;/p&gt;</content>
+    <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="https://mastodon.social/users/amrrr"/>
+    <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
+    <mastodon:scope>public</mastodon:scope>
+    <link rel="alternate" type="text/html" href="https://mastodon.social/users/lambadalambda/updates/1512350"/>
+    <link rel="self" type="application/atom+xml" href="https://mastodon.social/users/lambadalambda/updates/1512350.atom"/>
+    <thr:in-reply-to ref="tag:mastodon.social,2017-04-03:objectId=1476030:objectType=Status" href="https://mastodon.social/@amrrr/1476030"/>
+  </entry>
+  <entry>
+    <id>tag:mastodon.social,2017-04-03:objectId=1475949:objectType=Status</id>
+    <published>2017-04-03T07:35:45Z</published>
+    <updated>2017-04-03T07:35:45Z</updated>
+    <title>New status by lambadalambda</title>
+    <activity:object-type>http://activitystrea.ms/schema/1.0/comment</activity:object-type>
+    <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
+    <content type="html" xml:lang="en">&lt;p&gt;&lt;span class="h-card"&gt;&lt;a href="https://mastodon.social/@Shookaite" class="u-url mention"&gt;@&lt;span&gt;Shookaite&lt;/span&gt;&lt;/a&gt;&lt;/span&gt; Oh, you mean like userstyles?&lt;/p&gt;</content>
+    <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="https://mastodon.social/users/Shookaite"/>
+    <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
+    <mastodon:scope>public</mastodon:scope>
+    <link rel="alternate" type="text/html" href="https://mastodon.social/users/lambadalambda/updates/1512271"/>
+    <link rel="self" type="application/atom+xml" href="https://mastodon.social/users/lambadalambda/updates/1512271.atom"/>
+    <thr:in-reply-to ref="tag:mastodon.social,2017-04-03:objectId=1475879:objectType=Status" href="https://mastodon.social/@Shookaite/1475879"/>
+  </entry>
+  <entry>
+    <id>tag:mastodon.social,2017-04-03:objectId=1475581:objectType=Status</id>
+    <published>2017-04-03T07:20:03Z</published>
+    <updated>2017-04-03T07:20:03Z</updated>
+    <title>New status by lambadalambda</title>
+    <activity:object-type>http://activitystrea.ms/schema/1.0/comment</activity:object-type>
+    <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
+    <content type="html" xml:lang="en">&lt;p&gt;&lt;span class="h-card"&gt;&lt;a href="https://mastodon.social/@Shookaite" class="u-url mention"&gt;@&lt;span&gt;Shookaite&lt;/span&gt;&lt;/a&gt;&lt;/span&gt; Would be nice if someone helped port Pleroma to Mastodon, that has a theme switcher (click on the cog in the upper right): &lt;a href="https://pleroma.heldscal.la/main/all" rel="nofollow noopener" target="_blank"&gt;&lt;span class="invisible"&gt;https://&lt;/span&gt;&lt;span class=""&gt;pleroma.heldscal.la/main/all&lt;/span&gt;&lt;span class="invisible"&gt;&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;</content>
+    <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="https://mastodon.social/users/Shookaite"/>
+    <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
+    <mastodon:scope>public</mastodon:scope>
+    <link rel="alternate" type="text/html" href="https://mastodon.social/users/lambadalambda/updates/1511987"/>
+    <link rel="self" type="application/atom+xml" href="https://mastodon.social/users/lambadalambda/updates/1511987.atom"/>
+    <thr:in-reply-to ref="tag:mastodon.social,2017-04-03:objectId=1475550:objectType=Status" href="https://mastodon.social/@Shookaite/1475550"/>
+  </entry>
+  <entry>
+    <id>tag:mastodon.social,2017-04-02:objectId=1457325:objectType=Status</id>
+    <published>2017-04-02T21:57:43Z</published>
+    <updated>2017-04-02T21:57:43Z</updated>
+    <title>New status by lambadalambda</title>
+    <activity:object-type>http://activitystrea.ms/schema/1.0/comment</activity:object-type>
+    <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
+    <content type="html" xml:lang="en">&lt;p&gt;&lt;span class="h-card"&gt;&lt;a href="https://mastodon.social/@rhosyn" class="u-url mention"&gt;@&lt;span&gt;rhosyn&lt;/span&gt;&lt;/a&gt;&lt;/span&gt; &lt;span class="h-card"&gt;&lt;a href="https://mastodon.social/@Meaningness" class="u-url mention"&gt;@&lt;span&gt;Meaningness&lt;/span&gt;&lt;/a&gt;&lt;/span&gt; you could take a look at those listed at social.guhnoo.org&lt;/p&gt;</content>
+    <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="https://mastodon.social/users/rhosyn"/>
+    <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="https://mastodon.social/users/Meaningness"/>
+    <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
+    <mastodon:scope>public</mastodon:scope>
+    <link rel="alternate" type="text/html" href="https://mastodon.social/users/lambadalambda/updates/1496564"/>
+    <link rel="self" type="application/atom+xml" href="https://mastodon.social/users/lambadalambda/updates/1496564.atom"/>
+    <thr:in-reply-to ref="tag:mastodon.social,2017-04-02:objectId=1449283:objectType=Status" href="https://mastodon.social/@rhosyn/1449283"/>
+  </entry>
+  <entry>
+    <id>tag:mastodon.social,2017-04-02:objectId=1447926:objectType=Status</id>
+    <published>2017-04-02T18:31:52Z</published>
+    <updated>2017-04-02T18:31:52Z</updated>
+    <title>New status by lambadalambda</title>
+    <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
+    <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
+    <content type="html" xml:lang="en">&lt;p&gt;My main account is &lt;span class="h-card"&gt;&lt;a href="https://social.heldscal.la/lambadalambda" class="u-url mention"&gt;@&lt;span&gt;lambadalambda&lt;/span&gt;&lt;/a&gt;&lt;/span&gt; , btw.&lt;/p&gt;</content>
+    <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="https://social.heldscal.la/user/23211"/>
+    <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
+    <mastodon:scope>public</mastodon:scope>
+    <link rel="alternate" type="text/html" href="https://mastodon.social/users/lambadalambda/updates/1488648"/>
+    <link rel="self" type="application/atom+xml" href="https://mastodon.social/users/lambadalambda/updates/1488648.atom"/>
+  </entry>
+  <entry>
+    <id>tag:mastodon.social,2017-04-02:objectId=1447878:objectType=Status</id>
+    <published>2017-04-02T18:30:37Z</published>
+    <updated>2017-04-02T18:30:37Z</updated>
+    <title>lambadalambda shared a status by Firstaide@awoo.space</title>
+    <activity:object-type>http://activitystrea.ms/schema/1.0/activity</activity:object-type>
+    <activity:verb>http://activitystrea.ms/schema/1.0/share</activity:verb>
+    <activity:object>
+      <id>tag:awoo.space,2017-04-02:objectId=135324:objectType=Status</id>
+      <published>2017-04-02T18:29:32Z</published>
+      <updated>2017-04-02T18:29:32Z</updated>
+      <title>New status by Firstaide@awoo.space</title>
+      <author>
+        <id>https://awoo.space/users/Firstaide</id>
+        <activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
+        <uri>https://awoo.space/users/Firstaide</uri>
+        <name>Firstaide</name>
+        <email>Firstaide@awoo.space</email>
+        <summary>A smol awoo account, for a smol autistic 💙
+They/them please!
+NB/white/ace</summary>
+        <link rel="alternate" type="text/html" href="https://awoo.space/@Firstaide"/>
+        <link rel="avatar" type="image/png" media:width="120" media:height="120" href="https://files.mastodon.social/accounts/avatars/000/023/707/original/95e92639771fd225.png?1492022811"/>
+        <link rel="header" type="image/jpeg" media:width="700" media:height="335" href="https://files.mastodon.social/accounts/headers/000/023/707/original/e98df174c26747be.jpg?1491667928"/>
+        <poco:preferredUsername>Firstaide</poco:preferredUsername>
+        <poco:displayName>Miff🚑✨</poco:displayName>
+        <poco:note>A smol awoo account, for a smol autistic 💙
+They/them please!
+NB/white/ace</poco:note>
+        <mastodon:scope>public</mastodon:scope>
+      </author>
+      <activity:object-type>http://activitystrea.ms/schema/1.0/comment</activity:object-type>
+      <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
+      <content type="html" xml:lang="en">&lt;p&gt;&lt;a href="https://mastodon.social/users/lambadalambda" class="h-card u-url p-nickname mention"&gt;@&lt;span&gt;lambadalambda&lt;/span&gt;&lt;/a&gt; yeah, I think that's p much the  big issue here? &lt;br&gt;When I first heard of Masto, I thought it was just like twitter at first, I had no idea federation was even a thing?, and I actually joined p early on? :-o &lt;/p&gt;&lt;p&gt;idk I think more stuff needs to be done about federation promotion, but honestly its gotta come from the get go when people get here to make an account I feel :-o&lt;/p&gt;</content>
+      <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="https://mastodon.social/users/lambadalambda"/>
+      <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
+      <mastodon:scope>public</mastodon:scope>
+      <link rel="alternate" type="text/html" href="https://awoo.space/users/Firstaide/updates/10904"/>
+      <thr:in-reply-to ref="tag:mastodon.social,2017-04-02:objectId=1447682:objectType=Status" href="https://mastodon.social/@lambadalambda/1447682"/>
+    </activity:object>
+    <content type="html" xml:lang="en">&lt;p&gt;&lt;a href="https://mastodon.social/users/lambadalambda" class="h-card u-url p-nickname mention"&gt;@&lt;span&gt;lambadalambda&lt;/span&gt;&lt;/a&gt; yeah, I think that's p much the  big issue here? &lt;br&gt;When I first heard of Masto, I thought it was just like twitter at first, I had no idea federation was even a thing?, and I actually joined p early on? :-o &lt;/p&gt;&lt;p&gt;idk I think more stuff needs to be done about federation promotion, but honestly its gotta come from the get go when people get here to make an account I feel :-o&lt;/p&gt;</content>
+    <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
+    <mastodon:scope>public</mastodon:scope>
+    <link rel="alternate" type="text/html" href="https://mastodon.social/users/lambadalambda/updates/1488609"/>
+    <link rel="self" type="application/atom+xml" href="https://mastodon.social/users/lambadalambda/updates/1488609.atom"/>
+  </entry>
+</feed>
diff --git a/test/fixtures/ostatus_incoming_post.xml b/test/fixtures/ostatus_incoming_post.xml
new file mode 100644 (file)
index 0000000..7967e1b
--- /dev/null
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<feed xml:lang="en-US" xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0" xmlns:georss="http://www.georss.org/georss" xmlns:activity="http://activitystrea.ms/spec/1.0/" xmlns:media="http://purl.org/syndication/atommedia" xmlns:poco="http://portablecontacts.net/spec/1.0" xmlns:ostatus="http://ostatus.org/schema/1.0" xmlns:statusnet="http://status.net/schema/api/1/">
+ <generator uri="https://gnu.io/social" version="1.0.2-dev">GNU social</generator>
+ <id>https://social.heldscal.la/api/statuses/user_timeline/23211.atom</id>
+ <title>lambadalambda timeline</title>
+ <subtitle>Updates from lambadalambda on social.heldscal.la!</subtitle>
+ <logo>https://social.heldscal.la/avatar/23211-96-20170416114255.jpeg</logo>
+ <updated>2017-04-29T18:25:38+00:00</updated>
+<author>
+ <activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
+ <uri>https://social.heldscal.la/user/23211</uri>
+ <name>lambadalambda</name>
+ <summary>Call me Deacon Blues.</summary>
+ <link rel="alternate" type="text/html" href="https://social.heldscal.la/lambadalambda"/>
+ <link rel="avatar" type="image/jpeg" media:width="236" media:height="236" href="https://social.heldscal.la/avatar/23211-original-20170416114255.jpeg"/>
+ <link rel="avatar" type="image/jpeg" media:width="96" media:height="96" href="https://social.heldscal.la/avatar/23211-96-20170416114255.jpeg"/>
+ <link rel="avatar" type="image/jpeg" media:width="48" media:height="48" href="https://social.heldscal.la/avatar/23211-48-20170416114255.jpeg"/>
+ <link rel="avatar" type="image/jpeg" media:width="24" media:height="24" href="https://social.heldscal.la/avatar/23211-24-20170416114257.jpeg"/>
+ <poco:preferredUsername>lambadalambda</poco:preferredUsername>
+ <poco:displayName>Constance Variable</poco:displayName>
+ <poco:note>Call me Deacon Blues.</poco:note>
+ <poco:address>
+  <poco:formatted>Berlin</poco:formatted>
+ </poco:address>
+ <poco:urls>
+  <poco:type>homepage</poco:type>
+  <poco:value>https://heldscal.la</poco:value>
+  <poco:primary>true</poco:primary>
+ </poco:urls>
+ <followers url="https://social.heldscal.la/lambadalambda/subscribers"></followers>
+ <statusnet:profile_info local_id="23211"></statusnet:profile_info>
+</author>
+ <link href="https://social.heldscal.la/lambadalambda" rel="alternate" type="text/html"/>
+ <link href="https://social.heldscal.la/main/sup" rel="http://api.friendfeed.com/2008/03#sup" type="application/json"/>
+ <link href="https://social.heldscal.la/main/push/hub" rel="hub"/>
+ <link href="https://social.heldscal.la/main/salmon/user/23211" rel="salmon"/>
+ <link href="https://social.heldscal.la/main/salmon/user/23211" rel="http://salmon-protocol.org/ns/salmon-replies"/>
+ <link href="https://social.heldscal.la/main/salmon/user/23211" rel="http://salmon-protocol.org/ns/salmon-mention"/>
+ <link href="https://social.heldscal.la/api/statuses/user_timeline/23211.atom" rel="self" type="application/atom+xml"/>
+<entry>
+ <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
+ <id>tag:social.heldscal.la,2017-04-29:noticeId=1967725:objectType=note</id>
+ <title>New note by lambadalambda</title>
+ <content type="html">Will it blend?</content>
+ <link rel="alternate" type="text/html" href="https://social.heldscal.la/notice/1967725"/>
+ <status_net notice_id="1967725"></status_net>
+ <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
+ <published>2017-04-29T18:25:38+00:00</published>
+ <updated>2017-04-29T18:25:38+00:00</updated>
+ <link rel="ostatus:conversation" href="https://social.heldscal.la/conversation/1007861"/>
+ <ostatus:conversation href="https://social.heldscal.la/conversation/1007861" local_id="1007861" ref="tag:social.heldscal.la,2017-04-29:objectType=thread:nonce=3f3a9dd83acc4e35">tag:social.heldscal.la,2017-04-29:objectType=thread:nonce=3f3a9dd83acc4e35</ostatus:conversation>
+ <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
+ <link rel="self" type="application/atom+xml" href="https://social.heldscal.la/api/statuses/show/1967725.atom"/>
+ <link rel="edit" type="application/atom+xml" href="https://social.heldscal.la/api/statuses/show/1967725.atom"/>
+ <statusnet:notice_info local_id="1967725" source="Pleroma FE"></statusnet:notice_info>
+</entry>
+</feed>
diff --git a/test/fixtures/ostatus_incoming_reply.xml b/test/fixtures/ostatus_incoming_reply.xml
new file mode 100644 (file)
index 0000000..83a427a
--- /dev/null
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<feed xml:lang="en-US" xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0" xmlns:georss="http://www.georss.org/georss" xmlns:activity="http://activitystrea.ms/spec/1.0/" xmlns:media="http://purl.org/syndication/atommedia" xmlns:poco="http://portablecontacts.net/spec/1.0" xmlns:ostatus="http://ostatus.org/schema/1.0" xmlns:statusnet="http://status.net/schema/api/1/">
+ <generator uri="https://gnu.io/social" version="1.0.2-dev">GNU social</generator>
+ <id>https://social.heldscal.la/api/statuses/user_timeline/23211.atom</id>
+ <title>lambadalambda timeline</title>
+ <subtitle>Updates from lambadalambda on social.heldscal.la!</subtitle>
+ <logo>https://social.heldscal.la/avatar/23211-96-20170416114255.jpeg</logo>
+ <updated>2017-04-30T09:30:32+00:00</updated>
+<author>
+ <activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
+ <uri>https://social.heldscal.la/user/23211</uri>
+ <name>lambadalambda</name>
+ <summary>Call me Deacon Blues.</summary>
+ <link rel="alternate" type="text/html" href="https://social.heldscal.la/lambadalambda"/>
+ <link rel="avatar" type="image/jpeg" media:width="236" media:height="236" href="https://social.heldscal.la/avatar/23211-original-20170416114255.jpeg"/>
+ <link rel="avatar" type="image/jpeg" media:width="96" media:height="96" href="https://social.heldscal.la/avatar/23211-96-20170416114255.jpeg"/>
+ <link rel="avatar" type="image/jpeg" media:width="48" media:height="48" href="https://social.heldscal.la/avatar/23211-48-20170416114255.jpeg"/>
+ <link rel="avatar" type="image/jpeg" media:width="24" media:height="24" href="https://social.heldscal.la/avatar/23211-24-20170416114257.jpeg"/>
+ <poco:preferredUsername>lambadalambda</poco:preferredUsername>
+ <poco:displayName>Constance Variable</poco:displayName>
+ <poco:note>Call me Deacon Blues.</poco:note>
+ <poco:address>
+  <poco:formatted>Berlin</poco:formatted>
+ </poco:address>
+ <poco:urls>
+  <poco:type>homepage</poco:type>
+  <poco:value>https://heldscal.la</poco:value>
+  <poco:primary>true</poco:primary>
+ </poco:urls>
+ <followers url="https://social.heldscal.la/lambadalambda/subscribers"></followers>
+ <statusnet:profile_info local_id="23211"></statusnet:profile_info>
+</author>
+ <link href="https://social.heldscal.la/lambadalambda" rel="alternate" type="text/html"/>
+ <link href="https://social.heldscal.la/main/sup" rel="http://api.friendfeed.com/2008/03#sup" type="application/json"/>
+ <link href="https://social.heldscal.la/main/push/hub" rel="hub"/>
+ <link href="https://social.heldscal.la/main/salmon/user/23211" rel="salmon"/>
+ <link href="https://social.heldscal.la/main/salmon/user/23211" rel="http://salmon-protocol.org/ns/salmon-replies"/>
+ <link href="https://social.heldscal.la/main/salmon/user/23211" rel="http://salmon-protocol.org/ns/salmon-mention"/>
+ <link href="https://social.heldscal.la/api/statuses/user_timeline/23211.atom" rel="self" type="application/atom+xml"/>
+<entry>
+ <activity:object-type>http://activitystrea.ms/schema/1.0/comment</activity:object-type>
+ <id>tag:social.heldscal.la,2017-04-30:noticeId=1978790:objectType=comment</id>
+ <title>New comment by lambadalambda</title>
+ <content type="html">@&lt;a href=&quot;https://gs.archae.me/user/4687&quot; class=&quot;h-card u-url p-nickname mention&quot; title=&quot;shpbot&quot;&gt;shpbot&lt;/a&gt; why not indeed.</content>
+ <link rel="alternate" type="text/html" href="https://social.heldscal.la/notice/1978790"/>
+ <status_net notice_id="1978790"></status_net>
+ <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
+ <published>2017-04-30T09:30:32+00:00</published>
+ <updated>2017-04-30T09:30:32+00:00</updated>
+ <thr:in-reply-to ref="tag:gs.archae.me,2017-04-30:noticeId=778260:objectType=note" href="https://gs.archae.me/notice/778260"></thr:in-reply-to>
+ <link rel="related" href="https://gs.archae.me/notice/778260"/>
+ <link rel="ostatus:conversation" href="https://social.heldscal.la/conversation/1013566"/>
+ <ostatus:conversation href="https://social.heldscal.la/conversation/1013566" local_id="1013566" ref="https://gs.archae.me/conversation/327120">https://gs.archae.me/conversation/327120</ostatus:conversation>
+ <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="https://gs.archae.me/user/4687"/>
+ <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
+ <link rel="self" type="application/atom+xml" href="https://social.heldscal.la/api/statuses/show/1978790.atom"/>
+ <link rel="edit" type="application/atom+xml" href="https://social.heldscal.la/api/statuses/show/1978790.atom"/>
+ <statusnet:notice_info local_id="1978790" source="Pleroma FE"></statusnet:notice_info>
+</entry>
+</feed>
diff --git a/test/fixtures/private_key.pem b/test/fixtures/private_key.pem
new file mode 100644 (file)
index 0000000..7a4b146
--- /dev/null
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEAqnWeDtrqWasCKNXiuSq1tSCLI5H7BSvIROy5YfuGsXHrIlCq
+LdIm9QlIUUmIi9QyzgiGEDsPCCkA1UguCVgF/UrJ1+FvHcHsTELkkBu/yCl9mrgt
+WzTckhb6KjOhqtxi/TKgRaJ2Rlwz2bvH5sbCP9qffthitdxfh14KC5V0gqDt1xCy
+WgZo79vbYMcVkcQoh5uLtG64ksYFBMfgnLaSj7xg5i2qCDiIY7bqBujo5HllDqeo
+w3LXmsztt1cT8heXEjW0SYJvAHJK00OsG1kp4cqhfKzxLCHNGQJVHQxLOXy97I7o
+HOeuhbxPhjpGSBMgw7YFm3ODXviqf557eqFcaQIDAQABAoIBAC6f+VnK22sncXHF
+/zvyyL0AZ86U8XpanW7s6VA5wn/qzwwV0Fa0Mt+3aEaDvIuywSrF/hWWcegjfwzX
+r2/y2cCMomUgTopvLrk1WttoG68eWjLlydI2xVZYXpkIgmH/4juri1dAtuVL9wrJ
+aEZhe2SH4jSJ74Ya/y5BtLGycaoA9FHyIzHPTx52Ix2jWKWtKimW8J+aERi2uHdN
+7yTnLT2APhs5fnvNnn0tg85CI3Ny2GNiqmAail14yVfRz8Sf6qDIepH5Jfz9oll4
+I+GYUOLs6eTgkHXBn8LGhtHTE/9UJmb42OyWrW8X+nc/Mjz5xh0u/g1Gdp36oUMz
+OotfneECgYEA3cGfQxmxjEqSbXt9jbxiCukU7PmkDDQqBu97URC4N8qEcMF1wW7X
+AddU7Kq/UJU+oqjD/7UQHoS2ZThPtto6SpVdXQzsnrnPWQcrv5b1DV/TpXfwGoZ3
+svUIAcx4vGzhhmHDJCBsdY6n8xWBYtSqfLFXgN5UkdafLGy3EkCEtmUCgYEAxMgl
+7eU2QkWkzgJxOj6xjG2yqM3jxOvvoiRnD0rIQaBS70P/1N94ZkMXzOwddddZ5OW+
+55h/a8TmFKP/+NW4PHRYra/dazGI4IBlw6Yeq6uq/4jbuSqtBbaNn/Dz5kdHBTqM
+PtbBvc9Fztd2zb3InyyLbb4c+WjMqi0AooN027UCgYB4Tax7GJtLwsEBiDcrB4Ig
+7SYfEae/vyT1skIyTmHCUqnbCfk6QUl/hDRcWJ2FuBHM6MW8GZxvEgxpiU0lo+pv
+v+xwqKxNx/wHDm7bd6fl45DMee7WVRDnEyuO3kC56E/JOYxGMxjkBcpzg703wqvj
+Dcqs7PDwVYDw9uGykzHsSQKBgEQnNcvA+RvW1w9qlSChGgkS7S+9r0dCl8pGZVNM
+iTMBfffUS0TE6QQx9IpKtKFdpoq6b3XywR7oIO/BJSRfkOGPQi9Vm5BGpatrjNNI
+M5Mtb5n1InRtLWOvKDnez/pPcW+EKZKR+qPsp7bNtR3ovxUx7lBh6dMP0uKVl4Sx
+lsWJAoGBAIeek9eG+S3m2jaJRHasfKo5mJ2JrrmnjQXUOGUP8/CgO8sW1VmG2WAk
+Av7+BRI2mP2f+3SswG/AoRGmRXXw65ly63ws8ixrhK0MG3MgqDkWc69SbTaaMJ+u
+BQFYMsB1vZdUV3CaRqySkjY68QWGcJ4Z5JKHuTXzKv/GeFmw0V9R
+-----END RSA PRIVATE KEY-----
diff --git a/test/fixtures/salmon2.xml b/test/fixtures/salmon2.xml
new file mode 100644 (file)
index 0000000..d8ecbc1
--- /dev/null
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<me:env xmlns:me="http://salmon-protocol.org/ns/magic-env"><me:data type="application/atom+xml">PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiID8-PGVudHJ5IHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDA1L0F0b20iIHhtbG5zOnRocj0iaHR0cDovL3B1cmwub3JnL3N5bmRpY2F0aW9uL3RocmVhZC8xLjAiIHhtbG5zOmFjdGl2aXR5PSJodHRwOi8vYWN0aXZpdHlzdHJlYS5tcy9zcGVjLzEuMC8iIHhtbG5zOmdlb3Jzcz0iaHR0cDovL3d3dy5nZW9yc3Mub3JnL2dlb3JzcyIgeG1sbnM6b3N0YXR1cz0iaHR0cDovL29zdGF0dXMub3JnL3NjaGVtYS8xLjAiIHhtbG5zOnBvY289Imh0dHA6Ly9wb3J0YWJsZWNvbnRhY3RzLm5ldC9zcGVjLzEuMCIgeG1sbnM6bWVkaWE9Imh0dHA6Ly9wdXJsLm9yZy9zeW5kaWNhdGlvbi9hdG9tbWVkaWEiIHhtbG5zOnN0YXR1c25ldD0iaHR0cDovL3N0YXR1cy5uZXQvc2NoZW1hL2FwaS8xLyI-CiA8YWN0aXZpdHk6b2JqZWN0LXR5cGU-aHR0cDovL2FjdGl2aXR5c3RyZWEubXMvc2NoZW1hLzEuMC9ub3RlPC9hY3Rpdml0eTpvYmplY3QtdHlwZT4KIDxpZD50YWc6c29jaWFsLmhlbGRzY2FsLmxhLDIwMTctMDQtMjk6bm90aWNlSWQ9MTk2NzEwNjpvYmplY3RUeXBlPW5vdGU8L2lkPgogPHRpdGxlPk5ldyBub3RlIGJ5IGxhbWJhZGFsYW1iZGE8L3RpdGxlPgogPGNvbnRlbnQgdHlwZT0iaHRtbCI-dGVzdCBAJmx0O2EgaHJlZj0mcXVvdDtodHRwczovL3BsZXJvbWEuc295a2FmLmNvbS91c2Vycy9sYWluJnF1b3Q7IGNsYXNzPSZxdW90O2gtY2FyZCB1LXVybCBwLW5pY2tuYW1lIG1lbnRpb24mcXVvdDsmZ3Q7bGFpbiZsdDsvYSZndDs8L2NvbnRlbnQ-CiA8bGluayByZWw9ImFsdGVybmF0ZSIgdHlwZT0idGV4dC9odG1sIiBocmVmPSJodHRwczovL3NvY2lhbC5oZWxkc2NhbC5sYS9ub3RpY2UvMTk2NzEwNiIvPgogPHN0YXR1c19uZXQgbm90aWNlX2lkPSIxOTY3MTA2Ij48L3N0YXR1c19uZXQ-CiA8YWN0aXZpdHk6dmVyYj5odHRwOi8vYWN0aXZpdHlzdHJlYS5tcy9zY2hlbWEvMS4wL3Bvc3Q8L2FjdGl2aXR5OnZlcmI-CiA8cHVibGlzaGVkPjIwMTctMDQtMjlUMTc6Mjg6MjErMDA6MDA8L3B1Ymxpc2hlZD4KIDx1cGRhdGVkPjIwMTctMDQtMjlUMTc6Mjg6MjErMDA6MDA8L3VwZGF0ZWQ-CiA8YXV0aG9yPgogIDxhY3Rpdml0eTpvYmplY3QtdHlwZT5odHRwOi8vYWN0aXZpdHlzdHJlYS5tcy9zY2hlbWEvMS4wL3BlcnNvbjwvYWN0aXZpdHk6b2JqZWN0LXR5cGU-CiAgPHVyaT5odHRwczovL3NvY2lhbC5oZWxkc2NhbC5sYS91c2VyLzIzMjExPC91cmk-CiAgPG5hbWU-bGFtYmFkYWxhbWJkYTwvbmFtZT4KICA8c3VtbWFyeT5DYWxsIG1lIERlYWNvbiBCbHVlcy48L3N1bW1hcnk-CiAgPGxpbmsgcmVsPSJhbHRlcm5hdGUiIHR5cGU9InRleHQvaHRtbCIgaHJlZj0iaHR0cHM6Ly9zb2NpYWwuaGVsZHNjYWwubGEvbGFtYmFkYWxhbWJkYSIvPgogIDxsaW5rIHJlbD0iYXZhdGFyIiB0eXBlPSJpbWFnZS9qcGVnIiBtZWRpYTp3aWR0aD0iMjM2IiBtZWRpYTpoZWlnaHQ9IjIzNiIgaHJlZj0iaHR0cHM6Ly9zb2NpYWwuaGVsZHNjYWwubGEvYXZhdGFyLzIzMjExLW9yaWdpbmFsLTIwMTcwNDE2MTE0MjU1LmpwZWciLz4KICA8bGluayByZWw9ImF2YXRhciIgdHlwZT0iaW1hZ2UvanBlZyIgbWVkaWE6d2lkdGg9Ijk2IiBtZWRpYTpoZWlnaHQ9Ijk2IiBocmVmPSJodHRwczovL3NvY2lhbC5oZWxkc2NhbC5sYS9hdmF0YXIvMjMyMTEtOTYtMjAxNzA0MTYxMTQyNTUuanBlZyIvPgogIDxsaW5rIHJlbD0iYXZhdGFyIiB0eXBlPSJpbWFnZS9qcGVnIiBtZWRpYTp3aWR0aD0iNDgiIG1lZGlhOmhlaWdodD0iNDgiIGhyZWY9Imh0dHBzOi8vc29jaWFsLmhlbGRzY2FsLmxhL2F2YXRhci8yMzIxMS00OC0yMDE3MDQxNjExNDI1NS5qcGVnIi8-CiAgPGxpbmsgcmVsPSJhdmF0YXIiIHR5cGU9ImltYWdlL2pwZWciIG1lZGlhOndpZHRoPSIyNCIgbWVkaWE6aGVpZ2h0PSIyNCIgaHJlZj0iaHR0cHM6Ly9zb2NpYWwuaGVsZHNjYWwubGEvYXZhdGFyLzIzMjExLTI0LTIwMTcwNDE2MTE0MjU3LmpwZWciLz4KICA8cG9jbzpwcmVmZXJyZWRVc2VybmFtZT5sYW1iYWRhbGFtYmRhPC9wb2NvOnByZWZlcnJlZFVzZXJuYW1lPgogIDxwb2NvOmRpc3BsYXlOYW1lPkNvbnN0YW5jZSBWYXJpYWJsZTwvcG9jbzpkaXNwbGF5TmFtZT4KICA8cG9jbzpub3RlPkNhbGwgbWUgRGVhY29uIEJsdWVzLjwvcG9jbzpub3RlPgogIDxwb2NvOmFkZHJlc3M-CiAgIDxwb2NvOmZvcm1hdHRlZD5CZXJsaW48L3BvY286Zm9ybWF0dGVkPgogIDwvcG9jbzphZGRyZXNzPgogIDxwb2NvOnVybHM-CiAgIDxwb2NvOnR5cGU-aG9tZXBhZ2U8L3BvY286dHlwZT4KICAgPHBvY286dmFsdWU-aHR0cHM6Ly9oZWxkc2NhbC5sYTwvcG9jbzp2YWx1ZT4KICAgPHBvY286cHJpbWFyeT50cnVlPC9wb2NvOnByaW1hcnk-CiAgPC9wb2NvOnVybHM-CiAgPGZvbGxvd2VycyB1cmw9Imh0dHBzOi8vc29jaWFsLmhlbGRzY2FsLmxhL2xhbWJhZGFsYW1iZGEvc3Vic2NyaWJlcnMiPjwvZm9sbG93ZXJzPgogIDxzdGF0dXNuZXQ6cHJvZmlsZV9pbmZvIGxvY2FsX2lkPSIyMzIxMSI-PC9zdGF0dXNuZXQ6cHJvZmlsZV9pbmZvPgogPC9hdXRob3I-CiA8bGluayByZWw9Im9zdGF0dXM6Y29udmVyc2F0aW9uIiBocmVmPSJodHRwczovL3NvY2lhbC5oZWxkc2NhbC5sYS9jb252ZXJzYXRpb24vMTAwNzQ5NiIvPgogPG9zdGF0dXM6Y29udmVyc2F0aW9uIGhyZWY9Imh0dHBzOi8vc29jaWFsLmhlbGRzY2FsLmxhL2NvbnZlcnNhdGlvbi8xMDA3NDk2IiBsb2NhbF9pZD0iMTAwNzQ5NiIgcmVmPSJ0YWc6c29jaWFsLmhlbGRzY2FsLmxhLDIwMTctMDQtMjk6b2JqZWN0VHlwZT10aHJlYWQ6bm9uY2U9NDU5ZGYyMjM2NDFiMDNkZSI-dGFnOnNvY2lhbC5oZWxkc2NhbC5sYSwyMDE3LTA0LTI5Om9iamVjdFR5cGU9dGhyZWFkOm5vbmNlPTQ1OWRmMjIzNjQxYjAzZGU8L29zdGF0dXM6Y29udmVyc2F0aW9uPgogPGxpbmsgcmVsPSJtZW50aW9uZWQiIG9zdGF0dXM6b2JqZWN0LXR5cGU9Imh0dHA6Ly9hY3Rpdml0eXN0cmVhLm1zL3NjaGVtYS8xLjAvcGVyc29uIiBocmVmPSJodHRwczovL3BsZXJvbWEuc295a2FmLmNvbS91c2Vycy9sYWluIi8-CiA8bGluayByZWw9Im1lbnRpb25lZCIgb3N0YXR1czpvYmplY3QtdHlwZT0iaHR0cDovL2FjdGl2aXR5c3RyZWEubXMvc2NoZW1hLzEuMC9jb2xsZWN0aW9uIiBocmVmPSJodHRwOi8vYWN0aXZpdHlzY2hlbWEub3JnL2NvbGxlY3Rpb24vcHVibGljIi8-CiA8c291cmNlPgogIDxpZD5odHRwczovL3NvY2lhbC5oZWxkc2NhbC5sYS9hcGkvc3RhdHVzZXMvdXNlcl90aW1lbGluZS8yMzIxMS5hdG9tPC9pZD4KICA8dGl0bGU-Q29uc3RhbmNlIFZhcmlhYmxlPC90aXRsZT4KICA8bGluayByZWw9ImFsdGVybmF0ZSIgdHlwZT0idGV4dC9odG1sIiBocmVmPSJodHRwczovL3NvY2lhbC5oZWxkc2NhbC5sYS9sYW1iYWRhbGFtYmRhIi8-CiAgPGxpbmsgcmVsPSJzZWxmIiB0eXBlPSJhcHBsaWNhdGlvbi9hdG9tK3htbCIgaHJlZj0iaHR0cHM6Ly9zb2NpYWwuaGVsZHNjYWwubGEvYXBpL3N0YXR1c2VzL3VzZXJfdGltZWxpbmUvMjMyMTEuYXRvbSIvPgogIDxsaW5rIHJlbD0ibGljZW5zZSIgaHJlZj0iaHR0cHM6Ly9jcmVhdGl2ZWNvbW1vbnMub3JnL2xpY2Vuc2VzL2J5LzMuMC8iLz4KICA8aWNvbj5odHRwczovL3NvY2lhbC5oZWxkc2NhbC5sYS9hdmF0YXIvMjMyMTEtOTYtMjAxNzA0MTYxMTQyNTUuanBlZzwvaWNvbj4KICA8dXBkYXRlZD4yMDE3LTA0LTI5VDE3OjI4OjIxKzAwOjAwPC91cGRhdGVkPgogPC9zb3VyY2U-CiA8bGluayByZWw9InNlbGYiIHR5cGU9ImFwcGxpY2F0aW9uL2F0b20reG1sIiBocmVmPSJodHRwczovL3NvY2lhbC5oZWxkc2NhbC5sYS9hcGkvc3RhdHVzZXMvc2hvdy8xOTY3MTA2LmF0b20iLz4KIDxsaW5rIHJlbD0iZWRpdCIgdHlwZT0iYXBwbGljYXRpb24vYXRvbSt4bWwiIGhyZWY9Imh0dHBzOi8vc29jaWFsLmhlbGRzY2FsLmxhL2FwaS9zdGF0dXNlcy9zaG93LzE5NjcxMDYuYXRvbSIvPgogPHN0YXR1c25ldDpub3RpY2VfaW5mbyBsb2NhbF9pZD0iMTk2NzEwNiIgc291cmNlPSJQbGVyb21hIEZFIj48L3N0YXR1c25ldDpub3RpY2VfaW5mbz4KPC9lbnRyeT4K</me:data><me:encoding>base64url</me:encoding><me:alg>RSA-SHA256</me:alg><me:sig>CJ3wiWW9Io6Y24To3PFBF8cGuvJG8ps5zEwu1k1kSAlSX7WcysvS4ZoPKICFrD4brJxMLpW3AQCLNPIa246-Y0noGiNdpj0w0_TWgWXukWo50pD7cWVugr15YCMUtC-v00iDYfZTlmrTVM6kSCcpAmGMbZPTaXVmKZryjTDoXSI=</me:sig></me:env>
\ No newline at end of file
diff --git a/test/fixtures/share-gs.xml b/test/fixtures/share-gs.xml
new file mode 100644 (file)
index 0000000..ab5e488
--- /dev/null
@@ -0,0 +1,99 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<feed xml:lang="en-US" xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0" xmlns:georss="http://www.georss.org/georss" xmlns:activity="http://activitystrea.ms/spec/1.0/" xmlns:media="http://purl.org/syndication/atommedia" xmlns:poco="http://portablecontacts.net/spec/1.0" xmlns:ostatus="http://ostatus.org/schema/1.0" xmlns:statusnet="http://status.net/schema/api/1/">
+ <generator uri="https://gnu.io/social" version="1.0.2-dev">GNU social</generator>
+ <id>https://social.heldscal.la/api/statuses/user_timeline/23211.atom</id>
+ <title>lambadalambda timeline</title>
+ <subtitle>Updates from lambadalambda on social.heldscal.la!</subtitle>
+ <logo>https://social.heldscal.la/avatar/23211-96-20170416114255.jpeg</logo>
+ <updated>2017-05-03T08:05:41+00:00</updated>
+<author>
+ <activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
+ <uri>https://social.heldscal.la/user/23211</uri>
+ <name>lambadalambda</name>
+ <summary>Call me Deacon Blues.</summary>
+ <link rel="alternate" type="text/html" href="https://social.heldscal.la/lambadalambda"/>
+ <link rel="avatar" type="image/jpeg" media:width="236" media:height="236" href="https://social.heldscal.la/avatar/23211-original-20170416114255.jpeg"/>
+ <link rel="avatar" type="image/jpeg" media:width="96" media:height="96" href="https://social.heldscal.la/avatar/23211-96-20170416114255.jpeg"/>
+ <link rel="avatar" type="image/jpeg" media:width="48" media:height="48" href="https://social.heldscal.la/avatar/23211-48-20170416114255.jpeg"/>
+ <link rel="avatar" type="image/jpeg" media:width="24" media:height="24" href="https://social.heldscal.la/avatar/23211-24-20170416114257.jpeg"/>
+ <poco:preferredUsername>lambadalambda</poco:preferredUsername>
+ <poco:displayName>Constance Variable</poco:displayName>
+ <poco:note>Call me Deacon Blues.</poco:note>
+ <poco:address>
+  <poco:formatted>Berlin</poco:formatted>
+ </poco:address>
+ <poco:urls>
+  <poco:type>homepage</poco:type>
+  <poco:value>https://heldscal.la</poco:value>
+  <poco:primary>true</poco:primary>
+ </poco:urls>
+ <followers url="https://social.heldscal.la/lambadalambda/subscribers"></followers>
+ <statusnet:profile_info local_id="23211"></statusnet:profile_info>
+</author>
+ <link href="https://social.heldscal.la/lambadalambda" rel="alternate" type="text/html"/>
+ <link href="https://social.heldscal.la/main/sup" rel="http://api.friendfeed.com/2008/03#sup" type="application/json"/>
+ <link href="https://social.heldscal.la/main/push/hub" rel="hub"/>
+ <link href="https://social.heldscal.la/main/salmon/user/23211" rel="salmon"/>
+ <link href="https://social.heldscal.la/main/salmon/user/23211" rel="http://salmon-protocol.org/ns/salmon-replies"/>
+ <link href="https://social.heldscal.la/main/salmon/user/23211" rel="http://salmon-protocol.org/ns/salmon-mention"/>
+ <link href="https://social.heldscal.la/api/statuses/user_timeline/23211.atom" rel="self" type="application/atom+xml"/>
+<entry>
+ <id>tag:social.heldscal.la,2017-05-03:noticeId=2028428:objectType=note</id>
+ <title>lambadalambda repeated a notice by lain</title>
+ <content type="html">RT @&lt;a href=&quot;https://pleroma.soykaf.com/users/lain&quot; class=&quot;h-card u-url p-nickname mention&quot; title=&quot;Lain Iwakura&quot;&gt;lain&lt;/a&gt; Added returning the entries as xml... let's see if the mastodon hammering stops now.</content>
+ <link rel="alternate" type="text/html" href="https://social.heldscal.la/notice/2028428"/>
+ <activity:verb>http://activitystrea.ms/schema/1.0/share</activity:verb>
+ <published>2017-05-03T08:05:41+00:00</published>
+ <updated>2017-05-03T08:05:41+00:00</updated>
+ <activity:object>
+  <activity:object-type>http://activitystrea.ms/schema/1.0/activity</activity:object-type>
+  <id>https://pleroma.soykaf.com/objects/4c1bda26-902e-4525-9fcd-b9fd44925193</id>
+  <title></title>
+  <content type="html">Added returning the entries as xml... let's see if the mastodon hammering stops now.</content>
+  <link rel="alternate" type="text/html" href="https://pleroma.soykaf.com/objects/4c1bda26-902e-4525-9fcd-b9fd44925193"/>
+  <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
+  <published>2017-05-03T08:04:44+00:00</published>
+  <updated>2017-05-03T08:04:44+00:00</updated>
+  <author>
+   <activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
+   <uri>https://pleroma.soykaf.com/users/lain</uri>
+   <name>lain</name>
+   <summary>Test account</summary>
+   <link rel="alternate" type="text/html" href="https://pleroma.soykaf.com/users/lain"/>
+   <link rel="avatar" type="image/jpeg" media:width="250" media:height="202" href="https://social.heldscal.la/avatar/43188-original-20170429171039.jpeg"/>
+   <link rel="avatar" type="image/jpeg" media:width="96" media:height="96" href="https://social.heldscal.la/avatar/43188-96-20170429172422.jpeg"/>
+   <link rel="avatar" type="image/jpeg" media:width="48" media:height="48" href="https://social.heldscal.la/avatar/43188-48-20170429172422.jpeg"/>
+   <link rel="avatar" type="image/jpeg" media:width="24" media:height="24" href="https://social.heldscal.la/avatar/43188-24-20170429181411.jpeg"/>
+   <poco:preferredUsername>lain</poco:preferredUsername>
+   <poco:displayName>Lain Iwakura</poco:displayName>
+   <poco:note>Test account</poco:note>
+   <statusnet:profile_info local_id="43188"></statusnet:profile_info>
+  </author>
+  <activity:object>
+   <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
+   <id>https://pleroma.soykaf.com/objects/4c1bda26-902e-4525-9fcd-b9fd44925193</id>
+   <title>New note by lain</title>
+   <content type="html">Added returning the entries as xml... let's see if the mastodon hammering stops now.</content>
+   <link rel="alternate" type="text/html" href="https://pleroma.soykaf.com/objects/4c1bda26-902e-4525-9fcd-b9fd44925193"/>
+   <status_net notice_id="2028424"></status_net>
+  </activity:object>
+  <link rel="ostatus:conversation" href="https://social.heldscal.la/conversation/1042737"/>
+  <ostatus:conversation href="https://social.heldscal.la/conversation/1042737" local_id="1042737" ref="https://pleroma.soykaf.com/contexts/ede39a2b-7cf3-4fa4-8ccd-cb97431bcc22">https://pleroma.soykaf.com/contexts/ede39a2b-7cf3-4fa4-8ccd-cb97431bcc22</ostatus:conversation>
+  <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
+  <source>
+   <id>https://pleroma.soykaf.com/users/lain/feed.atom</id>
+   <title>Lain Iwakura</title>
+   <link rel="alternate" type="text/html" href="https://pleroma.soykaf.com/users/lain"/>
+   <link rel="self" type="application/atom+xml" href="https://pleroma.soykaf.com/users/lain/feed.atom"/>
+   <icon>https://social.heldscal.la/avatar/43188-96-20170429172422.jpeg</icon>
+   <updated>2017-05-03T08:04:44+00:00</updated>
+  </source>
+ </activity:object>
+ <link rel="ostatus:conversation" href="https://social.heldscal.la/conversation/1042737"/>
+ <ostatus:conversation href="https://social.heldscal.la/conversation/1042737" local_id="1042737" ref="https://pleroma.soykaf.com/contexts/ede39a2b-7cf3-4fa4-8ccd-cb97431bcc22">https://pleroma.soykaf.com/contexts/ede39a2b-7cf3-4fa4-8ccd-cb97431bcc22</ostatus:conversation>
+ <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
+ <link rel="self" type="application/atom+xml" href="https://social.heldscal.la/api/statuses/show/2028428.atom"/>
+ <link rel="edit" type="application/atom+xml" href="https://social.heldscal.la/api/statuses/show/2028428.atom"/>
+ <statusnet:notice_info local_id="2028428" source="api" repeat_of="2028424"></statusnet:notice_info>
+</entry>
+</feed>
diff --git a/test/fixtures/share.xml b/test/fixtures/share.xml
new file mode 100644 (file)
index 0000000..e07b886
--- /dev/null
@@ -0,0 +1,54 @@
+<?xml version="1.0"?>
+<entry xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0" xmlns:activity="http://activitystrea.ms/spec/1.0/" xmlns:poco="http://portablecontacts.net/spec/1.0" xmlns:media="http://purl.org/syndication/atommedia" xmlns:ostatus="http://ostatus.org/schema/1.0" xmlns:mastodon="http://mastodon.social/schema/1.0">
+  <id>tag:mastodon.social,2017-05-03:objectId=4934452:objectType=Status</id>
+  <published>2017-05-03T08:21:09Z</published>
+  <updated>2017-05-03T08:21:09Z</updated>
+  <title>lambadalambda shared a status by lain@pleroma.soykaf.com</title>
+  <author>
+    <id>https://mastodon.social/users/lambadalambda</id>
+    <activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
+    <uri>https://mastodon.social/users/lambadalambda</uri>
+    <name>lambadalambda</name>
+    <email>lambadalambda@mastodon.social</email>
+    <link rel="alternate" type="text/html" href="https://mastodon.social/@lambadalambda"/>
+    <link rel="avatar" type="image/gif" media:width="120" media:height="120" href="https://files.mastodon.social/accounts/avatars/000/000/264/original/1429214160519.gif"/>
+    <link rel="header" type="" media:width="700" media:height="335" href="/headers/original/missing.png"/>
+    <poco:preferredUsername>lambadalambda</poco:preferredUsername>
+    <poco:displayName>Critical Value</poco:displayName>
+    <mastodon:scope>public</mastodon:scope>
+  </author>
+  <activity:object-type>http://activitystrea.ms/schema/1.0/activity</activity:object-type>
+  <activity:verb>http://activitystrea.ms/schema/1.0/share</activity:verb>
+  <activity:object>
+    <id>https://pleroma.soykaf.com/objects/4c1bda26-902e-4525-9fcd-b9fd44925193</id>
+    <published>2017-05-03T08:04:44Z</published>
+    <updated>2017-05-03T08:05:52Z</updated>
+    <title>New status by lain@pleroma.soykaf.com</title>
+    <author>
+      <id>https://pleroma.soykaf.com/users/lain</id>
+      <activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
+      <uri>https://pleroma.soykaf.com/users/lain</uri>
+      <name>lain</name>
+      <email>lain@pleroma.soykaf.com</email>
+      <summary type="html">Test account</summary>
+      <link rel="alternate" type="text/html" href="https://pleroma.soykaf.com/users/lain"/>
+      <link rel="avatar" type="image/jpeg" media:width="120" media:height="120" href="https://files.mastodon.social/accounts/avatars/000/125/902/original/6B3AFC74ACA841B24CFB94DB9044C84EDE6AFF31C71718B023D413DAED09A68E.jpeg"/>
+      <link rel="header" type="" media:width="700" media:height="335" href="/headers/original/missing.png"/>
+      <poco:preferredUsername>lain</poco:preferredUsername>
+      <poco:displayName>Lain Iwakura</poco:displayName>
+      <poco:note>Test account</poco:note>
+      <mastodon:scope>public</mastodon:scope>
+    </author>
+    <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
+    <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
+    <content type="html" xml:lang="en">Added returning the entries as xml... let's see if the mastodon hammering stops now.</content>
+    <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
+    <mastodon:scope>public</mastodon:scope>
+    <link rel="alternate" type="text/html" href=""/>
+  </activity:object>
+  <content type="html" xml:lang="en">Added returning the entries as xml... let's see if the mastodon hammering stops now.</content>
+  <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
+  <mastodon:scope>public</mastodon:scope>
+  <link rel="alternate" type="text/html" href="https://mastodon.social/users/lambadalambda/updates/2232660"/>
+  <link rel="self" type="application/atom+xml" href="https://mastodon.social/users/lambadalambda/updates/2232660.atom"/>
+</entry>
diff --git a/test/fixtures/user_full.xml b/test/fixtures/user_full.xml
new file mode 100644 (file)
index 0000000..8eee8c6
--- /dev/null
@@ -0,0 +1,10 @@
+<author>
+  <activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
+  <uri>http://gs.example.org:4040/index.php/user/1</uri>
+  <name>lambda</name>
+  <link rel="avatar" type="image/png" media:width="96" media:height="96" href="http://gs.example.org:4040/theme/neo-gnu/default-avatar-profile.png"/>
+  <link rel="avatar" type="image/png" media:width="48" media:height="48" href="http://gs.example.org:4040/theme/neo-gnu/default-avatar-stream.png"/>
+  <link rel="avatar" type="image/png" media:width="24" media:height="24" href="http://gs.example.org:4040/theme/neo-gnu/default-avatar-mini.png"/>
+  <poco:preferredUsername>Constance Variable</poco:preferredUsername>
+  <poco:displayName>lambadalambda</poco:displayName>
+</author>
diff --git a/test/fixtures/user_name_only.xml b/test/fixtures/user_name_only.xml
new file mode 100644 (file)
index 0000000..6d895d5
--- /dev/null
@@ -0,0 +1,5 @@
+<author>
+  <activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
+  <uri>http://gs.example.org:4040/index.php/user/1</uri>
+  <name>lambda</name>
+</author>
diff --git a/test/fixtures/webfinger.xml b/test/fixtures/webfinger.xml
new file mode 100644 (file)
index 0000000..4cde42e
--- /dev/null
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
+ <Subject>acct:shp@social.heldscal.la</Subject>
+ <Alias>https://social.heldscal.la/user/29191</Alias>
+ <Alias>https://social.heldscal.la/shp</Alias>
+ <Alias>https://social.heldscal.la/index.php/user/29191</Alias>
+ <Alias>https://social.heldscal.la/index.php/shp</Alias>
+ <Link rel="http://webfinger.net/rel/profile-page" type="text/html" href="https://social.heldscal.la/shp"/>
+ <Link rel="http://gmpg.org/xfn/11" type="text/html" href="https://social.heldscal.la/shp"/>
+ <Link rel="describedby" type="application/rdf+xml" href="https://social.heldscal.la/shp/foaf"/>
+ <Link rel="http://apinamespace.org/atom" type="application/atomsvc+xml" href="https://social.heldscal.la/api/statusnet/app/service/shp.xml"/>
+ <Link rel="http://apinamespace.org/twitter" href="https://social.heldscal.la/api/"/>
+ <Link rel="http://specs.openid.net/auth/2.0/provider" href="https://social.heldscal.la/shp"/>
+ <Link rel="http://schemas.google.com/g/2010#updates-from" type="application/atom+xml" href="https://social.heldscal.la/api/statuses/user_timeline/29191.atom"/>
+ <Link rel="magic-public-key" href="data:application/magic-public-key,RSA.wQ3i9UA0qmAxZ0WTIp4a-waZn_17Ez1pEEmqmqoooRsG1_BvpmOvLN0G2tEcWWxl2KOtdQMCiPptmQObeZeuj48mdsDZ4ArQinexY2hCCTcbV8Xpswpkb8K05RcKipdg07pnI7tAgQ0VWSZDImncL6YUGlG5YN8b5TjGOwk2VG8=.AQAB"/>
+ <Link rel="salmon" href="https://social.heldscal.la/main/salmon/user/29191"/>
+ <Link rel="http://salmon-protocol.org/ns/salmon-replies" href="https://social.heldscal.la/main/salmon/user/29191"/>
+ <Link rel="http://salmon-protocol.org/ns/salmon-mention" href="https://social.heldscal.la/main/salmon/user/29191"/>
+ <Link rel="http://ostatus.org/schema/1.0/subscribe" template="https://social.heldscal.la/main/ostatussub?profile={uri}"/>
+</XRD>
index 0f9cd0d15cae388bc0122bfa47cbab75f6aa9586..16011edbfda2d9d386e0034b19679032b917e758 100644 (file)
@@ -5,7 +5,7 @@ defmodule Pleroma.Builders.ActivityBuilder do
   def build(data \\ %{}, opts \\ %{}) do
     user = opts[:user] || Pleroma.Factory.insert(:user)
     activity = %{
-      "id" => 1,
+      "id" => Pleroma.Web.ActivityPub.ActivityPub.generate_object_id,
       "actor" => user.ap_id,
       "to" => ["https://www.w3.org/ns/activitystreams#Public"],
       "object" => %{
@@ -23,7 +23,7 @@ defmodule Pleroma.Builders.ActivityBuilder do
 
   def insert_list(times, data \\ %{}, opts \\ %{}) do
     Enum.map(1..times, fn (n) ->
-      {:ok, activity} = insert(Map.merge(data, %{"id" => n}))
+      {:ok, activity} = insert(data)
       activity
     end)
   end
index d7c16f0e0997edd92e609a4bdf00032064ef1d21..ac276567a0f6a1902514d393affd772049f3970a 100644 (file)
@@ -24,7 +24,8 @@ defmodule Pleroma.Factory do
       "to" => ["https://www.w3.org/ns/activitystreams#Public"],
       "published_at" => DateTime.utc_now() |> DateTime.to_iso8601,
       "likes" => [],
-      "like_count" => 0
+      "like_count" => 0,
+      "context" => "2hu"
     }
 
     %Pleroma.Object{
@@ -40,7 +41,8 @@ defmodule Pleroma.Factory do
       "actor" => note.data["actor"],
       "to" => note.data["to"],
       "object" => note.data,
-      "published_at" => DateTime.utc_now() |> DateTime.to_iso8601
+      "published_at" => DateTime.utc_now() |> DateTime.to_iso8601,
+      "context" => note.data["context"]
     }
 
     %Pleroma.Activity{
@@ -74,4 +76,14 @@ defmodule Pleroma.Factory do
       state: "requested"
     }
   end
+
+  def websub_client_subscription_factory do
+    %Pleroma.Web.Websub.WebsubClientSubscription{
+      topic: "http://example.org",
+      secret: "here's a secret",
+      valid_until: nil,
+      state: "requested",
+      subscribers: []
+    }
+  end
 end
index d711adb9db3ae1c1d9246675ef1001b16d89d052..036e70dff3cfec9b29db568e4d860a0e925d99c5 100644 (file)
@@ -13,7 +13,7 @@ defmodule Pleroma.UserTest do
 
     user = UserBuilder.build
 
-    expected_ap_id = "https://#{host}/users/#{user.nickname}"
+    expected_ap_id = "#{Pleroma.Web.base_url}/users/#{user.nickname}"
 
     assert expected_ap_id == User.ap_id(user)
   end
@@ -86,4 +86,40 @@ defmodule Pleroma.UserTest do
       assert changeset.changes[:following] == [User.ap_followers(%User{nickname: @full_user_data.nickname})]
     end
   end
+
+  describe "fetching a user from nickname or trying to build one" do
+    test "gets an existing user" do
+      user = insert(:user)
+      fetched_user = User.get_or_fetch_by_nickname(user.nickname)
+
+      assert user == fetched_user
+    end
+
+    # TODO: Make the test local.
+    test "fetches an external user via ostatus if no user exists" do
+      fetched_user = User.get_or_fetch_by_nickname("shp@social.heldscal.la")
+      assert fetched_user.nickname == "shp@social.heldscal.la"
+    end
+
+    test "returns nil if no user could be fetched" do
+      fetched_user = User.get_or_fetch_by_nickname("nonexistant@social.heldscal.la")
+      assert fetched_user == nil
+    end
+
+    test "returns nil for nonexistant local user" do
+      fetched_user = User.get_or_fetch_by_nickname("nonexistant")
+      assert fetched_user == nil
+    end
+  end
+
+  test "returns an ap_id for a user" do
+    user = insert(:user)
+    assert User.ap_id(user) == Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :feed_redirect, user.nickname)
+  end
+
+  test "returns an ap_followers link for a user" do
+    user = insert(:user)
+    assert User.ap_followers(user) == Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :feed_redirect, user.nickname) <> "/followers"
+  end
 end
+
index 744021c8cb21a4414b5cf453951d6b47b86a156d..dfa73b775bde52c338a8c92e1727b9aa81f32945 100644 (file)
@@ -40,6 +40,13 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
     end
   end
 
+  describe "create activities" do
+    test "removes doubled 'to' recipients" do
+      {:ok, activity} = ActivityPub.create(["user1", "user1", "user2"], %User{ap_id: "1"}, "", %{})
+      assert activity.data["to"] == ["user1", "user2"]
+    end
+  end
+
   describe "fetch activities for recipients" do
     test "retrieve the activities for certain recipients" do
       {:ok, activity_one} = ActivityBuilder.insert(%{"to" => ["someone"]})
@@ -125,6 +132,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
       assert like_activity.data["type"] == "Like"
       assert like_activity.data["object"] == object.data["id"]
       assert like_activity.data["to"] == [User.ap_followers(user), note_activity.data["actor"]]
+      assert like_activity.data["context"] == object.data["context"]
       assert object.data["like_count"] == 1
       assert object.data["likes"] == [user.ap_id]
 
@@ -174,6 +182,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
       assert announce_activity.data["to"] == [User.ap_followers(user), note_activity.data["actor"]]
       assert announce_activity.data["object"] == object.data["id"]
       assert announce_activity.data["actor"] == user.ap_id
+      assert announce_activity.data["context"] == object.data["context"]
     end
   end
 
index 61df41a1d56772c014e66c2369130463cba6c6e2..12c9bbaa25d3b40718fe599968c464d8bdb7a174 100644 (file)
@@ -2,7 +2,8 @@ defmodule Pleroma.Web.OStatus.ActivityRepresenterTest do
   use Pleroma.DataCase
 
   alias Pleroma.Web.OStatus.ActivityRepresenter
-  alias Pleroma.{User, Activity}
+  alias Pleroma.{User, Activity, Object}
+  alias Pleroma.Web.ActivityPub.ActivityPub
 
   import Pleroma.Factory
 
@@ -23,6 +24,10 @@ defmodule Pleroma.Web.OStatus.ActivityRepresenterTest do
     <content type="html">#{note_activity.data["object"]["content"]}</content>
     <published>#{inserted_at}</published>
     <updated>#{updated_at}</updated>
+    <ostatus:conversation>#{note_activity.data["context"]}</ostatus:conversation>
+    <link href="#{note_activity.data["context"]}" rel="ostatus:conversation" />
+    <link type="application/atom+xml" href="#{note_activity.data["object"]["id"]}" rel="self" />
+    <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
     """
 
     tuple = ActivityRepresenter.to_simple_form(note_activity, user)
@@ -32,6 +37,124 @@ defmodule Pleroma.Web.OStatus.ActivityRepresenterTest do
     assert clean(res) == clean(expected)
   end
 
+  test "a reply note" do
+    note = insert(:note_activity)
+    answer = insert(:note_activity)
+    object = answer.data["object"]
+    object = Map.put(object, "inReplyTo", note.data["object"]["id"])
+
+    data = %{answer.data | "object" => object}
+    answer = %{answer | data: data}
+
+    updated_at = answer.updated_at
+    |> NaiveDateTime.to_iso8601
+    inserted_at = answer.inserted_at
+    |> NaiveDateTime.to_iso8601
+
+    user = User.get_cached_by_ap_id(answer.data["actor"])
+
+    expected = """
+    <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
+    <activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
+    <id>#{answer.data["object"]["id"]}</id>
+    <title>New note by #{user.nickname}</title>
+    <content type="html">#{answer.data["object"]["content"]}</content>
+    <published>#{inserted_at}</published>
+    <updated>#{updated_at}</updated>
+    <ostatus:conversation>#{answer.data["context"]}</ostatus:conversation>
+    <link href="#{answer.data["context"]}" rel="ostatus:conversation" />
+    <link type="application/atom+xml" href="#{answer.data["object"]["id"]}" rel="self" />
+    <thr:in-reply-to ref="#{note.data["object"]["id"]}" />
+    <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
+    """
+
+    tuple = ActivityRepresenter.to_simple_form(answer, user)
+
+    res = :xmerl.export_simple_content(tuple, :xmerl_xml) |> IO.iodata_to_binary
+
+    assert clean(res) == clean(expected)
+  end
+
+  test "an announce activity" do
+    note = insert(:note_activity)
+    user = insert(:user)
+    object = Object.get_cached_by_ap_id(note.data["object"]["id"])
+
+    {:ok, announce, object} = ActivityPub.announce(user, object)
+
+    announce = Repo.get(Activity, announce.id)
+
+    note_user = User.get_cached_by_ap_id(note.data["actor"])
+    note = Repo.get(Activity, note.id)
+    note_xml = ActivityRepresenter.to_simple_form(note, note_user, true)
+    |> :xmerl.export_simple_content(:xmerl_xml)
+    |> to_string
+
+    updated_at = announce.updated_at
+    |> NaiveDateTime.to_iso8601
+    inserted_at = announce.inserted_at
+    |> NaiveDateTime.to_iso8601
+
+    expected = """
+    <activity:object-type>http://activitystrea.ms/schema/1.0/activity</activity:object-type>
+    <activity:verb>http://activitystrea.ms/schema/1.0/share</activity:verb>
+    <id>#{announce.data["id"]}</id>
+    <title>#{user.nickname} repeated a notice</title>
+    <content type="html">RT #{note.data["object"]["content"]}</content>
+    <published>#{inserted_at}</published>
+    <updated>#{updated_at}</updated>
+    <ostatus:conversation>#{announce.data["context"]}</ostatus:conversation>
+    <link href="#{announce.data["context"]}" rel="ostatus:conversation" />
+    <link rel="self" type="application/atom+xml" href="#{announce.data["id"]}"/>
+    <activity:object>
+      #{note_xml}
+    </activity:object>
+    <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="#{note.data["actor"]}"/>
+    """
+
+    announce_xml = ActivityRepresenter.to_simple_form(announce, user)
+    |> :xmerl.export_simple_content(:xmerl_xml)
+    |> to_string
+
+    assert clean(expected) == clean(announce_xml)
+  end
+
+  test "a like activity" do
+    note = insert(:note)
+    user = insert(:user)
+    {:ok, like, _note} = ActivityPub.like(user, note)
+
+    updated_at = like.updated_at
+    |> NaiveDateTime.to_iso8601
+    inserted_at = like.inserted_at
+    |> NaiveDateTime.to_iso8601
+
+    tuple = ActivityRepresenter.to_simple_form(like, user)
+    refute is_nil(tuple)
+
+    res = :xmerl.export_simple_content(tuple, :xmerl_xml) |> IO.iodata_to_binary
+
+    expected = """
+    <activity:verb>http://activitystrea.ms/schema/1.0/favorite</activity:verb>
+    <id>#{like.data["id"]}</id>
+    <title>New favorite by #{user.nickname}</title>
+    <content type="html">#{user.nickname} favorited something</content>
+    <published>#{inserted_at}</published>
+    <updated>#{updated_at}</updated>
+    <activity:object>
+      <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
+      <id>#{note.data["id"]}</id>
+    </activity:object>
+    <ostatus:conversation>#{like.data["context"]}</ostatus:conversation>
+    <link href="#{like.data["context"]}" rel="ostatus:conversation" />
+    <link rel="self" type="application/atom+xml" href="#{like.data["id"]}"/>
+    <thr:in-reply-to ref="#{note.data["id"]}" />
+    <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="#{note.data["actor"]}"/>
+    """
+
+    assert clean(res) == clean(expected)
+  end
+
   test "an unknown activity" do
     tuple = ActivityRepresenter.to_simple_form(%Activity{}, nil)
     assert is_nil(tuple)
index 9a02d8c16cae22ca3be501aa824fdf14ddc9416a..df5a964e26b9fd7f7d2f8f030f2347a45c10fc40 100644 (file)
@@ -22,12 +22,13 @@ defmodule Pleroma.Web.OStatus.FeedRepresenterTest do
     |> :xmerl.export_simple_content(:xmerl_xml)
 
     expected = """
-    <feed xmlns="http://www.w3.org/2005/Atom" xmlns:activity="http://activitystrea.ms/spec/1.0/" xmlns:poco="http://portablecontacts.net/spec/1.0">
+    <feed xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0" xmlns:activity="http://activitystrea.ms/spec/1.0/" xmlns:poco="http://portablecontacts.net/spec/1.0" xmlns:ostatus="http://ostatus.org/schema/1.0">
       <id>#{OStatus.feed_path(user)}</id>
       <title>#{user.nickname}'s timeline</title>
       <updated>#{most_recent_update}</updated>
       <link rel="hub" href="#{OStatus.pubsub_path(user)}" />
-      <link rel="self" href="#{OStatus.feed_path(user)}" />
+      <link rel="salmon" href="#{OStatus.salmon_path(user)}" />
+      <link rel="self" href="#{OStatus.feed_path(user)}" type="application/atom+xml" />
       <author>
         #{user_xml}
       </author>
index 229cd9b1e1c214980eafdb8b00f4e6e4df8ce7bd..8b7ca4d8933dcbbe5191ebf6d98672a79a7637a8 100644 (file)
@@ -12,4 +12,15 @@ defmodule Pleroma.Web.OStatus.OStatusControllerTest do
 
     assert response(conn, 200)
   end
+
+  test "gets an object", %{conn: conn} do
+    note_activity = insert(:note_activity)
+    [_, uuid] = hd Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["object"]["id"])
+    url = "/objects/#{uuid}"
+
+    conn = conn
+    |> get(url)
+
+    assert response(conn, 200)
+  end
 end
diff --git a/test/web/ostatus/ostatus_test.exs b/test/web/ostatus/ostatus_test.exs
new file mode 100644 (file)
index 0000000..e85d767
--- /dev/null
@@ -0,0 +1,194 @@
+defmodule Pleroma.Web.OStatusTest do
+  use Pleroma.DataCase
+  alias Pleroma.Web.OStatus
+  alias Pleroma.Web.XML
+  alias Pleroma.{Object, Repo}
+
+  test "don't insert create notes twice" do
+    incoming = File.read!("test/fixtures/incoming_note_activity.xml")
+    {:ok, [_activity]} = OStatus.handle_incoming(incoming)
+    assert {:ok, [{:error, "duplicate activity"}]} == OStatus.handle_incoming(incoming)
+  end
+
+  test "handle incoming note - GS, Salmon" 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["object"]["id"] == "tag:gs.example.org:4040,2017-04-23:noticeId=29:objectType=note"
+    assert activity.data["published"] == "2017-04-23T14:51:03+00:00"
+    assert activity.data["context"] == "tag:gs.example.org:4040,2017-04-23:objectType=thread:nonce=f09e22f58abd5c7b"
+    assert "http://pleroma.example.org:4000/users/lain3" in activity.data["to"]
+    assert activity.local == false
+  end
+
+  test "handle incoming notes - GS, subscription" do
+    incoming = File.read!("test/fixtures/ostatus_incoming_post.xml")
+    {:ok, [activity]} = OStatus.handle_incoming(incoming)
+
+    assert activity.data["type"] == "Create"
+    assert activity.data["object"]["type"] == "Note"
+    assert activity.data["object"]["actor"] == "https://social.heldscal.la/user/23211"
+    assert activity.data["object"]["content"] == "Will it blend?"
+  end
+
+  test "handle incoming notes with attachments - GS, subscription" do
+    incoming = File.read!("test/fixtures/incoming_websub_gnusocial_attachments.xml")
+    {:ok, [activity]} = OStatus.handle_incoming(incoming)
+
+    assert activity.data["type"] == "Create"
+    assert activity.data["object"]["type"] == "Note"
+    assert activity.data["object"]["actor"] == "https://social.heldscal.la/user/23211"
+    assert activity.data["object"]["attachment"] |> length == 2
+  end
+
+  test "handle incoming notes - Mastodon, salmon, reply" do
+    # It uses the context of the replied to object
+    Repo.insert!(%Object{
+          data: %{
+            "id" => "https://pleroma.soykaf.com/objects/c237d966-ac75-4fe3-a87a-d89d71a3a7a4",
+            "context" => "2hu"
+          }})
+    incoming = File.read!("test/fixtures/incoming_reply_mastodon.xml")
+    {:ok, [activity]} = OStatus.handle_incoming(incoming)
+
+    assert activity.data["type"] == "Create"
+    assert activity.data["object"]["type"] == "Note"
+    assert activity.data["object"]["actor"] == "https://mastodon.social/users/lambadalambda"
+    assert activity.data["context"] == "2hu"
+  end
+
+  test "handle incoming notes - GS, subscription, reply" do
+    incoming = File.read!("test/fixtures/ostatus_incoming_reply.xml")
+    {:ok, [activity]} = OStatus.handle_incoming(incoming)
+
+    assert activity.data["type"] == "Create"
+    assert activity.data["object"]["type"] == "Note"
+    assert activity.data["object"]["actor"] == "https://social.heldscal.la/user/23211"
+    assert activity.data["object"]["content"] == "@<a href=\"https://gs.archae.me/user/4687\" class=\"h-card u-url p-nickname mention\" title=\"shpbot\">shpbot</a> why not indeed."
+    assert activity.data["object"]["inReplyTo"] == "tag:gs.archae.me,2017-04-30:noticeId=778260:objectType=note"
+  end
+
+  test "handle incoming retweets - GS, subscription" do
+    incoming = File.read!("test/fixtures/share-gs.xml")
+    {:ok, [[activity, retweeted_activity]]} = OStatus.handle_incoming(incoming)
+
+    assert activity.data["type"] == "Announce"
+    assert activity.data["actor"] == "https://social.heldscal.la/user/23211"
+    assert activity.data["object"] == retweeted_activity.data["object"]["id"]
+    refute activity.local
+    assert retweeted_activity.data["type"] == "Create"
+    assert retweeted_activity.data["actor"] == "https://pleroma.soykaf.com/users/lain"
+    refute retweeted_activity.local
+  end
+
+  test "handle incoming retweets - Mastodon, salmon" do
+    incoming = File.read!("test/fixtures/share.xml")
+    {:ok, [[activity, retweeted_activity]]} = OStatus.handle_incoming(incoming)
+
+    assert activity.data["type"] == "Announce"
+    assert activity.data["actor"] == "https://mastodon.social/users/lambadalambda"
+    assert activity.data["object"] == retweeted_activity.data["object"]["id"]
+    refute activity.local
+    assert retweeted_activity.data["type"] == "Create"
+    assert retweeted_activity.data["actor"] == "https://pleroma.soykaf.com/users/lain"
+    refute retweeted_activity.local
+  end
+
+  test "handle incoming replies" do
+    incoming = File.read!("test/fixtures/incoming_note_activity_answer.xml")
+    {:ok, [activity]} = OStatus.handle_incoming(incoming)
+
+    assert activity.data["type"] == "Create"
+    assert activity.data["object"]["type"] == "Note"
+    assert activity.data["object"]["inReplyTo"] == "http://pleroma.example.org:4000/objects/55bce8fc-b423-46b1-af71-3759ab4670bc"
+    assert "http://pleroma.example.org:4000/users/lain5" in activity.data["to"]
+  end
+
+  describe "new remote user creation" do
+    test "tries to use the information in poco fields" do
+      # TODO make test local
+      uri = "https://social.heldscal.la/user/23211"
+
+      {:ok, user} = OStatus.find_or_make_user(uri)
+
+      user = Repo.get(Pleroma.User, user.id)
+      assert user.name == "Constance Variable"
+      assert user.nickname == "lambadalambda@social.heldscal.la"
+      assert user.local == false
+      assert user.info["uri"] == uri
+      assert user.ap_id == uri
+      assert user.avatar["type"] == "Image"
+
+      {:ok, user_again} = OStatus.find_or_make_user(uri)
+
+      assert user == user_again
+    end
+
+    test "find_make_or_update_user takes an author element and returns an updated user" do
+      # TODO make test local
+      uri = "https://social.heldscal.la/user/23211"
+
+      {:ok, user} = OStatus.find_or_make_user(uri)
+      change = Ecto.Changeset.change(user, %{avatar: nil})
+
+      {:ok, user} = Repo.update(change)
+      refute user.avatar
+
+      doc = XML.parse_document(File.read!("test/fixtures/23211.atom"))
+      [author] = :xmerl_xpath.string('//author[1]', doc)
+      {:ok, user} = OStatus.find_make_or_update_user(author)
+      assert user.avatar["type"] == "Image"
+
+      {:ok, user_again} = OStatus.find_make_or_update_user(author)
+      assert user_again == user
+    end
+  end
+
+  describe "gathering user info from a user id" do
+    test "it returns user info in a hash" do
+      user = "shp@social.heldscal.la"
+
+      # TODO: make test local
+      {:ok, data} = OStatus.gather_user_info(user)
+
+      expected = %{
+        "hub" => "https://social.heldscal.la/main/push/hub",
+        "magic_key" => "RSA.wQ3i9UA0qmAxZ0WTIp4a-waZn_17Ez1pEEmqmqoooRsG1_BvpmOvLN0G2tEcWWxl2KOtdQMCiPptmQObeZeuj48mdsDZ4ArQinexY2hCCTcbV8Xpswpkb8K05RcKipdg07pnI7tAgQ0VWSZDImncL6YUGlG5YN8b5TjGOwk2VG8=.AQAB",
+        "name" => "shp",
+        "nickname" => "shp",
+        "salmon" => "https://social.heldscal.la/main/salmon/user/29191",
+        "subject" => "acct:shp@social.heldscal.la",
+        "topic" => "https://social.heldscal.la/api/statuses/user_timeline/29191.atom",
+        "uri" => "https://social.heldscal.la/user/29191",
+        "host" => "social.heldscal.la",
+        "fqn" => user,
+        "avatar" => %{"type" => "Image", "url" => [%{"href" => "https://social.heldscal.la/avatar/29191-original-20170421154949.jpeg", "mediaType" => "image/jpeg", "type" => "Link"}]}
+      }
+      assert data == expected
+    end
+
+    test "it works with the uri" do
+      user = "https://social.heldscal.la/user/29191"
+
+      # TODO: make test local
+      {:ok, data} = OStatus.gather_user_info(user)
+
+      expected = %{
+        "hub" => "https://social.heldscal.la/main/push/hub",
+        "magic_key" => "RSA.wQ3i9UA0qmAxZ0WTIp4a-waZn_17Ez1pEEmqmqoooRsG1_BvpmOvLN0G2tEcWWxl2KOtdQMCiPptmQObeZeuj48mdsDZ4ArQinexY2hCCTcbV8Xpswpkb8K05RcKipdg07pnI7tAgQ0VWSZDImncL6YUGlG5YN8b5TjGOwk2VG8=.AQAB",
+        "name" => "shp",
+        "nickname" => "shp",
+        "salmon" => "https://social.heldscal.la/main/salmon/user/29191",
+        "subject" => "https://social.heldscal.la/user/29191",
+        "topic" => "https://social.heldscal.la/api/statuses/user_timeline/29191.atom",
+        "uri" => "https://social.heldscal.la/user/29191",
+        "host" => "social.heldscal.la",
+        "fqn" => user,
+        "avatar" => %{"type" => "Image", "url" => [%{"href" => "https://social.heldscal.la/avatar/29191-original-20170421154949.jpeg", "mediaType" => "image/jpeg", "type" => "Link"}]}
+      }
+      assert data == expected
+    end
+  end
+end
index 4ebb320814b21d95aba1029be7a979dd23d46f73..ed26ccf83af3a3c004653420d90ec42977311a19 100644 (file)
@@ -1,6 +1,8 @@
 defmodule Pleroma.Web.Salmon.SalmonTest do
   use Pleroma.DataCase
   alias Pleroma.Web.Salmon
+  alias Pleroma.{Repo, Activity, User}
+  import Pleroma.Factory
 
   @magickey "RSA.pu0s-halox4tu7wmES1FVSx6u-4wc0YrUFXcqWXZG4-27UmbCOpMQftRCldNRfyA-qLbz-eqiwQhh-1EwUvjsD4cYbAHNGHwTvDOyx5AKthQUP44ykPv7kjKGh3DWKySJvcs9tlUG87hlo7AvnMo9pwRS_Zz2CacQ-MKaXyDepk=.AQAB"
 
@@ -16,4 +18,75 @@ defmodule Pleroma.Web.Salmon.SalmonTest do
     {:ok, salmon} = File.read("test/fixtures/salmon.xml")
     assert Salmon.decode_and_validate(@wrong_magickey, salmon) == :error
   end
+
+  test "generates an RSA private key pem" do
+    {:ok, key} = Salmon.generate_rsa_pem
+    assert is_binary(key)
+    assert Regex.match?(~r/RSA/, key)
+  end
+
+  test "it encodes a magic key from a public key" do
+    key = Salmon.decode_key(@magickey)
+    magic_key = Salmon.encode_key(key)
+
+    assert @magickey == magic_key
+  end
+
+  test "returns a public and private key from a pem" do
+    pem = File.read!("test/fixtures/private_key.pem")
+    {:ok, private, public} = Salmon.keys_from_pem(pem)
+
+    assert elem(private, 0) == :RSAPrivateKey
+    assert elem(public, 0) == :RSAPublicKey
+  end
+
+  test "encodes an xml payload with a private key" do
+    doc = File.read!("test/fixtures/incoming_note_activity.xml")
+    pem = File.read!("test/fixtures/private_key.pem")
+    {:ok, private, public} = Salmon.keys_from_pem(pem)
+
+    # Let's try a roundtrip.
+    {:ok, salmon} = Salmon.encode(private, doc)
+    {:ok, decoded_doc} = Salmon.decode_and_validate(Salmon.encode_key(public), salmon)
+
+    assert doc == decoded_doc
+  end
+
+  test "it gets a magic key" do
+    # TODO: Make test local
+    salmon = File.read!("test/fixtures/salmon2.xml")
+    {:ok, key} = Salmon.fetch_magic_key(salmon)
+
+    assert key == "RSA.uzg6r1peZU0vXGADWxGJ0PE34WvmhjUmydbX5YYdOiXfODVLwCMi1umGoqUDm-mRu4vNEdFBVJU1CpFA7dKzWgIsqsa501i2XqElmEveXRLvNRWFB6nG03Q5OUY2as8eE54BJm0p20GkMfIJGwP6TSFb-ICp3QjzbatuSPJ6xCE=.AQAB"
+  end
+
+  test "it pushes an activity to remote accounts it's addressed to" do
+    user_data = %{
+      info: %{
+        "salmon" => "http://example.org/salmon"
+      },
+      local: false
+    }
+
+    mentioned_user = insert(:user, user_data)
+    note = insert(:note)
+    activity_data = %{
+      "id" => Pleroma.Web.ActivityPub.ActivityPub.generate_activity_id,
+      "type" => "Create",
+      "actor" => note.data["actor"],
+      "to" => note.data["to"] ++ [mentioned_user.ap_id],
+      "object" => note.data,
+      "published_at" => DateTime.utc_now() |> DateTime.to_iso8601,
+      "context" => note.data["context"]
+    }
+
+    {:ok, activity} = Repo.insert(%Activity{data: activity_data})
+    user = Repo.get_by(User, ap_id: activity.data["actor"])
+    {:ok, user} = Pleroma.Web.WebFinger.ensure_keys_present(user)
+
+    poster = fn (url, data, headers) ->
+      assert url == "http://example.org/salmon"
+    end
+    Salmon.publish(user, activity, poster)
+  end
 end
index d0cccb1497d84dff4ac3931446ba7d6019d46f15..64e7f0641b33148ef1a8de95fbd16569031a5f73 100644 (file)
@@ -69,6 +69,8 @@ defmodule Pleroma.Web.TwitterAPI.Representers.ActivityRepresenterTest do
     content = HtmlSanitizeEx.strip_tags(content_html)
     date = DateTime.from_naive!(~N[2016-05-24 13:26:08.003], "Etc/UTC") |> DateTime.to_iso8601
 
+    {:ok, convo_object} = Object.context_mapping("2hu") |> Repo.insert
+
     activity = %Activity{
       id: 1,
       data: %{
@@ -84,14 +86,15 @@ defmodule Pleroma.Web.TwitterAPI.Representers.ActivityRepresenterTest do
           "type" => "Note",
           "content" => content_html,
           "inReplyToStatusId" => 213123,
-          "statusnetConversationId" => 4711,
           "attachment" => [
             object
           ],
           "like_count" => 5,
-          "announcement_count" => 3
+          "announcement_count" => 3,
+          "context" => "2hu"
         },
-        "published" => date
+        "published" => date,
+        "context" => "2hu"
       }
     }
 
@@ -106,7 +109,7 @@ defmodule Pleroma.Web.TwitterAPI.Representers.ActivityRepresenterTest do
       "is_post_verb" => true,
       "created_at" => "Tue May 24 13:26:08 +0000 2016",
       "in_reply_to_status_id" => 213123,
-      "statusnet_conversation_id" => 4711,
+      "statusnet_conversation_id" => convo_object.id,
       "attachments" => [
         ObjectRepresenter.to_map(object)
       ],
index 1e92c519046391e6dffbaed50e9187a90e1f3a99..77f0659483f3ba1e851727d9d5bd3f82c760c0ab 100644 (file)
@@ -48,7 +48,8 @@ defmodule Pleroma.Web.TwitterAPI.Representers.UserRepresenterTest do
       "profile_image_url_profile_size" => image,
       "profile_image_url_original" => image,
       "following" => false,
-      "rights" => %{}
+      "rights" => %{},
+      "statusnet_profile_url" => user.ap_id
     }
 
     assert represented == UserRepresenter.to_map(user)
@@ -72,7 +73,8 @@ defmodule Pleroma.Web.TwitterAPI.Representers.UserRepresenterTest do
       "profile_image_url_profile_size" => image,
       "profile_image_url_original" => image,
       "following" => true,
-      "rights" => %{}
+      "rights" => %{},
+      "statusnet_profile_url" => user.ap_id
     }
 
     assert represented == UserRepresenter.to_map(user, %{for: follower})
index 6c249be7d619a0807437b3ce5e795f080cc6e46f..05cd084b4fb9cb2c70837bb403a56a0a089f77fc 100644 (file)
@@ -84,12 +84,13 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do
   describe "GET /statusnet/conversation/:id.json" do
     test "returns the statuses in the conversation", %{conn: conn} do
       {:ok, _user} = UserBuilder.insert
-      {:ok, _activity} = ActivityBuilder.insert(%{"statusnetConversationId" => 1, "context" => "2hu"})
-      {:ok, _activity_two} = ActivityBuilder.insert(%{"statusnetConversationId" => 1,"context" => "2hu"})
+      {:ok, _activity} = ActivityBuilder.insert(%{"context" => "2hu"})
+      {:ok, _activity_two} = ActivityBuilder.insert(%{"context" => "2hu"})
       {:ok, _activity_three} = ActivityBuilder.insert(%{"context" => "3hu"})
 
+      {:ok, object} = Object.context_mapping("2hu") |> Repo.insert
       conn = conn
-      |> get("/api/statusnet/conversation/1.json")
+      |> get("/api/statusnet/conversation/#{object.id}.json")
 
       response = json_response(conn, 200)
 
index 590428423b2e3a23e97476c3cfba61d572be328b..a92440f32feda2ac00b18f3021744d1994894f77 100644 (file)
@@ -33,19 +33,18 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
 
     { :ok, activity = %Activity{} } = TwitterAPI.create_status(user, input)
 
-    assert get_in(activity.data, ["object", "content"]) == "Hello again, <a href='shp'>@shp</a>.<br>This is on another line."
+    assert get_in(activity.data, ["object", "content"]) == "Hello again, <a href='shp'>@shp</a>.<br>This is on another line.<br><a href='http://example.org/image.jpg'>http://example.org/image.jpg</a>"
     assert get_in(activity.data, ["object", "type"]) == "Note"
     assert get_in(activity.data, ["object", "actor"]) == user.ap_id
     assert get_in(activity.data, ["actor"]) == user.ap_id
     assert Enum.member?(get_in(activity.data, ["to"]), User.ap_followers(user))
     assert Enum.member?(get_in(activity.data, ["to"]), "https://www.w3.org/ns/activitystreams#Public")
     assert Enum.member?(get_in(activity.data, ["to"]), "shp")
+    assert activity.local == true
 
-    # Add a context + 'statusnet_conversation_id'
+    # Add a context
     assert is_binary(get_in(activity.data, ["context"]))
     assert is_binary(get_in(activity.data, ["object", "context"]))
-    assert get_in(activity.data, ["object", "statusnetConversationId"]) == activity.id
-    assert get_in(activity.data, ["statusnetConversationId"]) == activity.id
 
     assert is_list(activity.data["object"]["attachment"])
 
@@ -69,15 +68,14 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
 
     assert get_in(reply.data, ["context"]) == get_in(activity.data, ["context"])
     assert get_in(reply.data, ["object", "context"]) == get_in(activity.data, ["object", "context"])
-    assert get_in(reply.data, ["statusnetConversationId"]) == get_in(activity.data, ["statusnetConversationId"])
-    assert get_in(reply.data, ["object", "statusnetConversationId"]) == get_in(activity.data, ["object", "statusnetConversationId"])
     assert get_in(reply.data, ["object", "inReplyTo"]) == get_in(activity.data, ["object", "id"])
     assert get_in(reply.data, ["object", "inReplyToStatusId"]) == activity.id
     assert Enum.member?(get_in(reply.data, ["to"]), "some_cool_id")
   end
 
-  test "fetch public statuses" do
+  test "fetch public statuses, excluding remote ones." do
     %{ public: activity, user: user } = ActivityBuilder.public_and_non_public
+    insert(:note_activity, %{local: false})
 
     follower = insert(:user, following: [User.ap_followers(user)])
 
@@ -87,6 +85,18 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
     assert Enum.at(statuses, 0) == ActivityRepresenter.to_map(activity, %{user: user, for: follower})
   end
 
+  test "fetch whole known network statuses" do
+    %{ public: activity, user: user } = ActivityBuilder.public_and_non_public
+    insert(:note_activity, %{local: false})
+
+    follower = insert(:user, following: [User.ap_followers(user)])
+
+    statuses = TwitterAPI.fetch_public_and_external_statuses(follower)
+
+    assert length(statuses) == 2
+    assert Enum.at(statuses, 0) == ActivityRepresenter.to_map(activity, %{user: user, for: follower})
+  end
+
   test "fetch friends' statuses" do
     user = insert(:user, %{following: ["someguy/followers"]})
     {:ok, activity} = ActivityBuilder.insert(%{"to" => ["someguy/followers"]})
@@ -201,11 +211,13 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
 
   test "fetch statuses in a context using the conversation id" do
     {:ok, user} = UserBuilder.insert()
-    {:ok, activity} = ActivityBuilder.insert(%{"statusnetConversationId" => 1, "context" => "2hu"})
-    {:ok, activity_two} = ActivityBuilder.insert(%{"statusnetConversationId" => 1,"context" => "2hu"})
+    {:ok, activity} = ActivityBuilder.insert(%{"context" => "2hu"})
+    {:ok, activity_two} = ActivityBuilder.insert(%{"context" => "2hu"})
     {:ok, _activity_three} = ActivityBuilder.insert(%{"context" => "3hu"})
 
-    statuses = TwitterAPI.fetch_conversation(user, 1)
+    {:ok, object} = Object.context_mapping("2hu") |> Repo.insert
+
+    statuses = TwitterAPI.fetch_conversation(user, object.id)
 
     assert length(statuses) == 2
     assert Enum.at(statuses, 0)["id"] == activity.id
@@ -314,9 +326,33 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
     refute Repo.get_by(User, nickname: "lain")
   end
 
+  test "it assigns an integer conversation_id" do
+    note_activity = insert(:note_activity)
+    user = User.get_cached_by_ap_id(note_activity.data["actor"])
+    status = ActivityRepresenter.to_map(note_activity, %{user: user})
+
+    assert is_number(status["statusnet_conversation_id"])
+  end
+
   setup do
     Supervisor.terminate_child(Pleroma.Supervisor, Cachex)
     Supervisor.restart_child(Pleroma.Supervisor, Cachex)
     :ok
   end
+
+  describe "context_to_conversation_id" do
+    test "creates a mapping object" do
+      conversation_id = TwitterAPI.context_to_conversation_id("random context")
+      object = Object.get_by_ap_id("random context")
+
+      assert conversation_id == object.id
+    end
+
+    test "returns an existing mapping for an existing object" do
+      {:ok, object} = Object.context_mapping("random context") |> Repo.insert
+      conversation_id = TwitterAPI.context_to_conversation_id("random context")
+
+      assert conversation_id == object.id
+    end
+  end
 end
index 8a3007ff9181f89491d81376bf557ef2a923a889..495d3d50b4c80e558d94b0ae88229048b5101ff3 100644 (file)
@@ -1,11 +1,61 @@
 defmodule Pleroma.Web.WebFingerTest do
   use Pleroma.DataCase
+  alias Pleroma.Web.WebFinger
+  import Pleroma.Factory
 
   describe "host meta" do
     test "returns a link to the xml lrdd" do
-      host_info = Pleroma.Web.WebFinger.host_meta
+      host_info = WebFinger.host_meta()
 
       assert String.contains?(host_info, Pleroma.Web.base_url)
     end
   end
+
+  describe "incoming webfinger request" do
+    test "works for fqns" do
+      user = insert(:user)
+
+      {:ok, result} = WebFinger.webfinger("#{user.nickname}@#{Pleroma.Web.Endpoint.host}")
+      assert is_binary(result)
+    end
+
+    test "works for ap_ids" do
+      user = insert(:user)
+
+      {:ok, result} = WebFinger.webfinger(user.ap_id)
+      assert is_binary(result)
+    end
+  end
+
+  describe "fingering" do
+    test "returns the info for a user" do
+      user = "shp@social.heldscal.la"
+
+      getter = fn(_url, _headers, [params: [resource: ^user]]) ->
+        {:ok, %{status_code: 200, body: File.read!("test/fixtures/webfinger.xml")}}
+      end
+
+      {:ok, data} = WebFinger.finger(user, getter)
+
+      assert data["magic_key"] == "RSA.wQ3i9UA0qmAxZ0WTIp4a-waZn_17Ez1pEEmqmqoooRsG1_BvpmOvLN0G2tEcWWxl2KOtdQMCiPptmQObeZeuj48mdsDZ4ArQinexY2hCCTcbV8Xpswpkb8K05RcKipdg07pnI7tAgQ0VWSZDImncL6YUGlG5YN8b5TjGOwk2VG8=.AQAB"
+      assert data["topic"] == "https://social.heldscal.la/api/statuses/user_timeline/29191.atom"
+      assert data["subject"] == "acct:shp@social.heldscal.la"
+      assert data["salmon"] == "https://social.heldscal.la/main/salmon/user/29191"
+    end
+  end
+
+  describe "ensure_keys_present" do
+    test "it creates keys for a user and stores them in info" do
+      user = insert(:user)
+      refute is_binary(user.info["keys"])
+      {:ok, user} = WebFinger.ensure_keys_present(user)
+      assert is_binary(user.info["keys"])
+    end
+
+    test "it doesn't create keys if there already are some" do
+      user = insert(:user, %{info: %{"keys" => "xxx"}})
+      {:ok, user} = WebFinger.ensure_keys_present(user)
+      assert user.info["keys"] == "xxx"
+    end
+  end
 end
index 8368cafea584ec76f1a49abba439d475253158f1..8f68248a486d2e97ac2aa676f767182ae38f4c74 100644 (file)
@@ -1,6 +1,9 @@
 defmodule Pleroma.Web.Websub.WebsubControllerTest do
   use Pleroma.Web.ConnCase
   import Pleroma.Factory
+  alias Pleroma.Web.Websub.WebsubClientSubscription
+  alias Pleroma.{Repo, Activity}
+  alias Pleroma.Web.Websub
 
   test "websub subscription request", %{conn: conn} do
     user = insert(:user)
@@ -20,4 +23,62 @@ defmodule Pleroma.Web.Websub.WebsubControllerTest do
 
     assert response(conn, 202) == "Accepted"
   end
+
+  test "websub subscription confirmation", %{conn: conn} do
+    websub = insert(:websub_client_subscription)
+
+    params = %{
+      "hub.mode" => "subscribe",
+      "hub.topic" => websub.topic,
+      "hub.challenge" => "some challenge",
+      "hub.lease_seconds" => 100
+    }
+
+    conn = conn
+    |> get("/push/subscriptions/#{websub.id}", params)
+
+    websub = Repo.get(WebsubClientSubscription, websub.id)
+
+    assert response(conn, 200) == "some challenge"
+    assert websub.state == "accepted"
+
+    # TODO valid_until
+  end
+
+  test "handles incoming feed updates", %{conn: conn} do
+    websub = insert(:websub_client_subscription)
+    doc = "some stuff"
+    signature = Websub.sign(websub.secret, doc)
+
+    conn = conn
+    |> put_req_header("x-hub-signature", "sha1=" <> signature)
+    |> put_req_header("content-type", "application/atom+xml")
+    |> post("/push/subscriptions/#{websub.id}", doc)
+
+    assert response(conn, 200) == "OK"
+
+    assert length(Repo.all(Activity)) == 1
+  end
+
+  test "rejects incoming feed updates with the wrong signature", %{conn: conn} do
+    websub = insert(:websub_client_subscription)
+    doc = "some stuff"
+    signature = Websub.sign("wrong secret", doc)
+
+    conn = conn
+    |> put_req_header("x-hub-signature", "sha1=" <> signature)
+    |> put_req_header("content-type", "application/atom+xml")
+    |> post("/push/subscriptions/#{websub.id}", doc)
+
+    assert response(conn, 500) == "Error"
+
+    assert length(Repo.all(Activity)) == 0
+  end
+end
+
+defmodule Pleroma.Web.OStatusMock do
+  import Pleroma.Factory
+  def handle_incoming(_doc) do
+    insert(:note_activity)
+  end
 end
index 334ba03fc28bea529635d8758b07ef7921323e2b..48774dc692c28d3a2d63eae381a836b771c0e241 100644 (file)
@@ -3,11 +3,13 @@ defmodule Pleroma.Web.WebsubMock do
     {:ok, sub}
   end
 end
+
 defmodule Pleroma.Web.WebsubTest do
   use Pleroma.DataCase
   alias Pleroma.Web.Websub
   alias Pleroma.Web.Websub.WebsubServerSubscription
   import Pleroma.Factory
+  alias Pleroma.Web.Router.Helpers
 
   test "a verification of a request that is accepted" do
     sub = insert(:websub_subscription)
@@ -58,7 +60,6 @@ defmodule Pleroma.Web.WebsubTest do
       "hub.lease_seconds" => "100"
     }
 
-
     {:ok, subscription } = Websub.incoming_subscription_request(user, data)
     assert subscription.topic == Pleroma.Web.OStatus.feed_path(user)
     assert subscription.state == "requested"
@@ -78,7 +79,6 @@ defmodule Pleroma.Web.WebsubTest do
       "hub.lease_seconds" => "100"
     }
 
-
     {:ok, subscription } = Websub.incoming_subscription_request(user, data)
     assert subscription.topic == Pleroma.Web.OStatus.feed_path(user)
     assert subscription.state == sub.state
@@ -87,4 +87,91 @@ defmodule Pleroma.Web.WebsubTest do
     assert length(Repo.all(WebsubServerSubscription)) == 1
     assert subscription.id == sub.id
   end
+
+  def accepting_verifier(subscription) do
+    {:ok, %{ subscription | state: "accepted" }}
+  end
+
+  test "initiate a subscription for a given user and topic" do
+    subscriber = insert(:user)
+    user = insert(:user, %{info: %{ "topic" =>  "some_topic", "hub" => "some_hub"}})
+
+    {:ok, websub} = Websub.subscribe(subscriber, user, &accepting_verifier/1)
+    assert websub.subscribers == [subscriber.ap_id]
+    assert websub.topic == "some_topic"
+    assert websub.hub == "some_hub"
+    assert is_binary(websub.secret)
+    assert websub.user == user
+    assert websub.state == "accepted"
+  end
+
+  test "discovers the hub and canonical url" do
+    topic = "https://mastodon.social/users/lambadalambda.atom"
+
+    getter = fn(^topic) ->
+      doc = File.read!("test/fixtures/lambadalambda.atom")
+      {:ok, %{status_code: 200, body: doc}}
+    end
+
+    {:ok, discovered} = Websub.gather_feed_data(topic, getter)
+    expected = %{
+      "hub" => "https://mastodon.social/api/push",
+      "uri" => "https://mastodon.social/users/lambadalambda",
+      "nickname" => "lambadalambda",
+      "name" => "Critical Value",
+      "host" => "mastodon.social",
+      "avatar" => %{"type" => "Image", "url" => [%{"href" => "https://files.mastodon.social/accounts/avatars/000/000/264/original/1429214160519.gif?1492379244", "mediaType" => "image/gif", "type" => "Link"}]}
+    }
+
+    assert expected == discovered
+  end
+
+  test "calls the hub, requests topic" do
+    hub = "https://social.heldscal.la/main/push/hub"
+    topic = "https://social.heldscal.la/api/statuses/user_timeline/23211.atom"
+    websub = insert(:websub_client_subscription, %{hub: hub, topic: topic})
+
+    poster = fn (^hub, {:form, data}, _headers) ->
+      assert Keyword.get(data, :"hub.mode") == "subscribe"
+      assert Keyword.get(data, :"hub.callback") == Helpers.websub_url(Pleroma.Web.Endpoint, :websub_subscription_confirmation, websub.id)
+      {:ok, %{status_code: 202}}
+    end
+
+    task = Task.async(fn -> Websub.request_subscription(websub, poster) end)
+
+    change = Ecto.Changeset.change(websub, %{state: "accepted"})
+    {:ok, _} = Repo.update(change)
+
+    {:ok, websub} = Task.await(task)
+
+    assert websub.state == "accepted"
+  end
+
+  test "rejects the subscription if it can't be accepted" do
+    hub = "https://social.heldscal.la/main/push/hub"
+    topic = "https://social.heldscal.la/api/statuses/user_timeline/23211.atom"
+    websub = insert(:websub_client_subscription, %{hub: hub, topic: topic})
+
+    poster = fn (^hub, {:form, _data}, _headers) ->
+      {:ok, %{status_code: 202}}
+    end
+
+    {:error, websub} = Websub.request_subscription(websub, poster, 1000)
+    assert websub.state == "rejected"
+
+    websub = insert(:websub_client_subscription, %{hub: hub, topic: topic})
+    poster = fn (^hub, {:form, _data}, _headers) ->
+      {:ok, %{status_code: 400}}
+    end
+
+    {:error, websub} = Websub.request_subscription(websub, poster, 1000)
+    assert websub.state == "rejected"
+  end
+
+  test "sign a text" do
+    signed = Websub.sign("secret", "text")
+    assert signed == "B8392C23690CCF871F37EC270BE1582DEC57A503" |> String.downcase
+
+    signed = Websub.sign("secret", [["て"], ['す']])
+  end
 end