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)
12 files changed:
1  2 
lib/pleroma/user.ex
lib/pleroma/web/activity_pub/activity_pub.ex
lib/pleroma/web/ostatus/activity_representer.ex
lib/pleroma/web/ostatus/feed_representer.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/twitter_api_controller.ex
lib/pleroma/web/web.ex
lib/pleroma/web/web_finger/web_finger.ex
lib/pleroma/web/websub/websub.ex

diff --combined lib/pleroma/user.ex
index 65925caed0aa3eee377b8264bbaa277e269b4bf9,01cbfe796379d864b997796a3879e731095ed4c9..23be6276ebe6abce6bffa9567d4c5a7d6225b600
@@@ -1,8 -1,9 +1,10 @@@
  defmodule Pleroma.User do
    use Ecto.Schema
 -  import Ecto.Changeset
 -  import Ecto.Query
 -  alias Pleroma.{Repo, User, Activity, Object}
++
 +  import Ecto.{Changeset, Query}
 +  alias Pleroma.{Repo, User, Object, Web}
 +  alias Comeonin.Pbkdf2
+   alias Pleroma.Web.OStatus
  
    schema "users" do
      field :bio, :string
      field :password_hash, :string
      field :password, :string, virtual: true
      field :password_confirmation, :string, virtual: true
 -    field :following, { :array, :string }, default: []
 +    field :following, {:array, :string}, default: []
      field :ap_id, :string
      field :avatar, :map
+     field :local, :boolean, default: true
+     field :info, :map, default: %{}
  
      timestamps()
    end
@@@ -27,7 -30,7 +31,7 @@@
    end
  
    def ap_id(%User{nickname: nickname}) do
 -    "#{Pleroma.Web.base_url}/users/#{nickname}"
 +    "#{Web.base_url}/users/#{nickname}"
    end
  
    def ap_followers(%User{} = user) do
@@@ -66,7 -69,7 +70,7 @@@
      |> validate_format(:nickname, ~r/^[a-zA-Z\d]+$/)
  
      if changeset.valid? do
 -      hashed = Comeonin.Pbkdf2.hashpwsalt(changeset.changes[:password])
 +      hashed = Pbkdf2.hashpwsalt(changeset.changes[:password])
        ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]})
        followers = User.ap_followers(%User{nickname: changeset.changes[:nickname]})
        changeset
@@@ -81,8 -84,8 +85,8 @@@
    def follow(%User{} = follower, %User{} = followed) do
      ap_followers = User.ap_followers(followed)
      if following?(follower, followed) do
 -      { :error,
 -        "Could not follow user: #{followed.nickname} is already on your list." }
 +      {:error,
 +       "Could not follow user: #{followed.nickname} is already on your list."}
      else
        following = [ap_followers | follower.following]
        |> Enum.uniq
        |> follow_changeset(%{following: following})
        |> Repo.update
      else
 -      { :error, "Not subscribed!" }
 +      {:error, "Not subscribed!"}
      end
    end
  
  
    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,1816b2e666c8476ec43491772b26978e40ea01c5..d7b490088da2902fe5cb9145c83cf746e72287b9
@@@ -1,9 -1,9 +1,9 @@@
  defmodule Pleroma.Web.ActivityPub.ActivityPub do
 -  alias Pleroma.Repo
 -  alias Pleroma.{Activity, Object, Upload, User}
 +  alias Pleroma.{Activity, Repo, Object, Upload, User, Web}
 +  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)
        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
 +  def like(%User{ap_id: ap_id} = user, %Object{data: %{"id" => id}} = object) do
      cond do
        # There's already a like here, so return the original activity.
        ap_id in (object.data["likes"] || []) ->
@@@ -33,7 -55,8 +55,8 @@@
            "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)
          |> Map.put("like_count", length(likes))
          |> Map.put("likes", likes)
  
 -        changeset = Ecto.Changeset.change(object, data: new_data)
 +        changeset = Changeset.change(object, data: new_data)
          {:ok, object} = Repo.update(changeset)
  
          update_object_in_activities(object)
  
+         if user.local do
+           Pleroma.Web.Federator.enqueue(:publish, activity)
+         end
          {:ok, activity, object}
      end
    end
@@@ -58,7 -85,7 +85,7 @@@
      relevant_activities = Activity.all_by_object_ap_id(id)
      Enum.map(relevant_activities, fn (activity) ->
        new_activity_data = activity.data |> Map.put("object", object.data)
 -      changeset = Ecto.Changeset.change(activity, data: new_activity_data)
 +      changeset = Changeset.change(activity, data: new_activity_data)
        Repo.update(changeset)
      end)
    end
        |> Map.put("like_count", length(likes))
        |> Map.put("likes", likes)
  
 -      changeset = Ecto.Changeset.change(object, data: new_data)
 +      changeset = Changeset.change(object, data: new_data)
        {:ok, object} = Repo.update(changeset)
  
        update_object_in_activities(object)
    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
 -    "#{Pleroma.Web.base_url()}/#{type}/#{Ecto.UUID.generate}"
 +    "#{Web.base_url()}/#{type}/#{UUID.generate}"
    end
  
    def fetch_public_activities(opts \\ %{}) 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
        query
      end
  
 -    Repo.all(query)
 -    |> Enum.reverse
 +    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
  
      |> Map.put("announcement_count", length(announcements))
      |> Map.put("announcements", announcements)
  
 -    changeset = Ecto.Changeset.change(object, data: new_data)
 +    changeset = Changeset.change(object, data: new_data)
      {:ok, object} = Repo.update(changeset)
  
      update_object_in_activities(object)
  
+     if user.local do
+       Pleroma.Web.Federator.enqueue(:publish, activity)
+     end
      {:ok, activity, object}
    end
  
index d7ea6132103b7eb96bc49ed40c7d022354e8982f,41a42b7cbb92c2d5d2cb2f55ab3ed813dcce50ca..88781626c889a20ddebd11953592bc3025f1c8bd
@@@ -1,5 -1,31 +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
        {: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
++  def to_simple_form(_, _, _), do: nil
  end
index 86c6f9d4f3fedc41c3254d395c41623aecd7d1be,7f9d6a46bab1420bf9ee12bbeb1e5c729d104e48..6b67b8ddf4f15485440e2e6dfac7a190318f37f1
@@@ -8,8 -8,7 +8,8 @@@ defmodule Pleroma.Web.OStatus.FeedRepre
  
      h = fn(str) -> [to_charlist(str)] end
  
 -    entries = Enum.map(activities, fn(activity) ->
 +    entries = activities
 +    |> Enum.map(fn(activity) ->
        {:entry, ActivityRepresenter.to_simple_form(activity, user)}
      end)
      |> Enum.filter(fn ({_, form}) -> form end)
      [{
        :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 ed7618d2b6836fc713467b90f90e513df8500c9a,1f2dedd3087069abde8a3ac6cd9e615c9ee81986..e442562d58c878b9d526fb52b2fe64833ddd45ab
@@@ -2,10 -2,16 +2,16 @@@ defmodule Pleroma.Web.OStatus.OStatusCo
    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,
@@@ -16,8 -22,7 +22,8 @@@
      activities = query
      |> Repo.all
  
 -    response = FeedRepresenter.to_simple_form(user, activities, [user])
 +    response = user
 +    |> FeedRepresenter.to_simple_form(activities, [user])
      |> :xmerl.export_simple(:xmerl_xml)
      |> to_string
  
      |> 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,ac9d97e0f558c9ed1a12c4d3e41119c85bc8e666..327987d1d9c1995364e925e6071ada01ae9d20f7
@@@ -1,7 -1,7 +1,7 @@@
  defmodule Pleroma.Web.Router do
    use Pleroma.Web, :router
  
 -  alias Pleroma.{Repo, User}
 +  alias Pleroma.{Repo, User, Web.Router}
  
    def user_fetcher(username) do
      {:ok, Repo.get_by(User, %{nickname: username})}
    pipeline :api do
      plug :accepts, ["json"]
      plug :fetch_session
 -    plug Pleroma.Plugs.AuthenticationPlug, %{fetcher: &Pleroma.Web.Router.user_fetcher/1, optional: true}
 +    plug Pleroma.Plugs.AuthenticationPlug, %{fetcher: &Router.user_fetcher/1, optional: true}
    end
  
    pipeline :authenticated_api do
      plug :accepts, ["json"]
      plug :fetch_session
 -    plug Pleroma.Plugs.AuthenticationPlug, %{fetcher: &Pleroma.Web.Router.user_fetcher/1}
 +    plug Pleroma.Plugs.AuthenticationPlug, %{fetcher: &Router.user_fetcher/1}
    end
  
    pipeline :well_known do
@@@ -30,7 -30,7 +30,7 @@@
      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
    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 +98,5 @@@ en
  
  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,f02cb11dca554bbf370e4b2b2d6eb6cbd7aad562..fe529c4c09ffa5450bd826a7951f3adda5bf851f
@@@ -1,8 -1,12 +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)
@@@ -10,6 -14,7 +14,6 @@@
      {:xmlObj, :string, encoding} = :xmerl_xpath.string('string(//me:encoding[1])', doc)
      {:xmlObj, :string, type} = :xmerl_xpath.string('string(//me:data[1]/@type)', doc)
  
 -
      {:ok, data} = Base.url_decode64(to_string(data), ignore: :whitespace)
      {:ok, sig} = Base.url_decode64(to_string(sig), ignore: :whitespace)
      alg = to_string(alg)
    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 -51,7 +50,7 @@@
      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)
  
      {: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,bfaabb4e424eee5f08bb2ae41af6aa7a1c55de0b..4d7ea0c5cfb21187c542df55ae6dec6376fa067f
@@@ -1,17 -1,17 +1,18 @@@
  defmodule Pleroma.Web.TwitterAPI.Representers.ActivityRepresenter do
    use Pleroma.Web.TwitterAPI.Representers.BaseRepresenter
    alias Pleroma.Web.TwitterAPI.Representers.{UserRepresenter, ObjectRepresenter}
 -  alias Pleroma.Activity
 +  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)
    end
  
 -  def to_map(%Activity{data: %{"type" => "Announce", "actor" => actor}} = activity, %{users: users, announced_activity: announced_activity} = opts) do
 +  def to_map(%Activity{data: %{"type" => "Announce", "actor" => actor, "published" => created_at}} = activity,
 +             %{users: users, announced_activity: announced_activity} = opts) do
      user = user_by_ap_id(users, actor)
 -    created_at = get_in(activity.data, ["published"])
 -    |> date_to_asctime
 +    created_at = created_at |> date_to_asctime
  
      text = "#{user.nickname} retweeted a status."
  
      }
    end
  
 -  def to_map(%Activity{data: %{"type" => "Like"}} = activity, %{user: user, liked_activity: liked_activity} = opts) do
 -    created_at = get_in(activity.data, ["published"])
 -    |> date_to_asctime
 +  def to_map(%Activity{data: %{"type" => "Like", "published" => created_at}} = activity,
 +             %{user: user, liked_activity: liked_activity} = opts) do
 +    created_at = created_at |> date_to_asctime
  
      text = "#{user.nickname} favorited a status."
  
      %{
        "id" => activity.id,
        "user" => UserRepresenter.to_map(user, opts),
 -      "statusnet_html" => text,  # TODO: add summary
 +      "statusnet_html" => text,
        "text" => text,
        "is_local" => true,
        "is_post_verb" => false,
      }
    end
  
 -  def to_map(%Activity{data: %{"type" => "Follow"}} = activity, %{user: user} = opts) do
 -    created_at = get_in(activity.data, ["published"])
 -    |> date_to_asctime
 +  def to_map(%Activity{data: %{"type" => "Follow", "published" => created_at, "object" => followed_id}} = activity, %{user: user} = opts) do
 +    created_at = created_at |> date_to_asctime
  
 +    followed = User.get_cached_by_ap_id(followed_id)
 +    text = "#{user.nickname} started following #{followed.nickname}"
      %{
        "id" => activity.id,
        "user" => UserRepresenter.to_map(user, opts),
        "attentions" => [],
 -      "statusnet_html" => "",  # TODO: add summary
 -      "text" => "",
 +      "statusnet_html" => text,
 +      "text" => text,
        "is_local" => true,
        "is_post_verb" => false,
        "created_at" => created_at,
      }
    end
  
 -  def to_map(%Activity{} = activity, %{user: user} = opts) do
 -    content = get_in(activity.data, ["object", "content"])
 -    created_at = get_in(activity.data, ["object", "published"])
 -    |> date_to_asctime
 -    like_count = get_in(activity.data, ["object", "like_count"]) || 0
 -    announcement_count = get_in(activity.data, ["object", "announcement_count"]) || 0
 -    favorited = opts[:for] && opts[:for].ap_id in (activity.data["object"]["likes"] || [])
 -    repeated = opts[:for] && opts[:for].ap_id in (activity.data["object"]["announcements"] || [])
 +  def to_map(%Activity{data: %{"object" => %{"content" => content} = object}} = activity, %{user: user} = opts) do
 +    created_at = object["published"] |> date_to_asctime
 +    like_count = object["like_count"] || 0
 +    announcement_count = object["announcement_count"] || 0
 +    favorited = opts[:for] && opts[:for].ap_id in (object["likes"] || [])
 +    repeated = opts[:for] && opts[:for].ap_id in (object["announcements"] || [])
  
      mentions = opts[:mentioned] || []
  
      |> 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),
        "is_local" => true,
        "is_post_verb" => true,
        "created_at" => created_at,
 -      "in_reply_to_status_id" => activity.data["object"]["inReplyToStatusId"],
 +      "in_reply_to_status_id" => object["inReplyToStatusId"],
-       "statusnet_conversation_id" => object["statusnetConversationId"],
+       "statusnet_conversation_id" => conversation_id,
 -      "attachments" => (activity.data["object"]["attachment"] || []) |> ObjectRepresenter.enum_to_list(opts),
 +      "attachments" => (object["attachment"] || []) |> ObjectRepresenter.enum_to_list(opts),
        "attentions" => attentions,
        "fave_num" => like_count,
        "repeat_num" => announcement_count,
 -      "favorited" => !!favorited,
 -      "repeated" => !!repeated,
 +      "favorited" => to_boolean(favorited),
 +      "repeated" => to_boolean(repeated),
      }
    end
  
    defp date_to_asctime(date) do
      with {:ok, date, _offset} <- date |> DateTime.from_iso8601 do
 -      Calendar.Strftime.strftime!(date, "%a %b %d %H:%M:%S %z %Y")
 +      Strftime.strftime!(date, "%a %b %d %H:%M:%S %z %Y")
      else _e ->
        ""
      end
    end
 +
 +  defp to_boolean(false) do
 +    false
 +  end
 +
 +  defp to_boolean(nil) do
 +    false
 +  end
 +
 +  defp to_boolean(_) do
 +    true
 +  end
  end
index bbfc04a6aa9657383de184ac948d737c8cff128a,4b329a21f791e018e880271c31df5f6dc4da4cb8..96a5f2151dd1b913f56064cfcc505519c1f0b629
@@@ -2,9 -2,8 +2,9 @@@ defmodule Pleroma.Web.TwitterAPI.Contro
    use Pleroma.Web, :controller
    alias Pleroma.Web.TwitterAPI.TwitterAPI
    alias Pleroma.Web.TwitterAPI.Representers.{UserRepresenter, ActivityRepresenter}
 -  alias Pleroma.{Repo, Activity}
 +  alias Pleroma.{Web, Repo, Activity}
    alias Pleroma.Web.ActivityPub.ActivityPub
 +  alias Ecto.Changeset
  
    def verify_credentials(%{assigns: %{user: user}} = conn, _params) do
      response = user |> UserRepresenter.to_json(%{for: user})
@@@ -16,7 -15,7 +16,7 @@@
    def status_update(%{assigns: %{user: user}} = conn, %{"status" => status_text} = status_data) do
      if status_text |> String.trim |> String.length != 0 do
        media_ids = extract_media_ids(status_data)
 -      {:ok, activity} = TwitterAPI.create_status(user, Map.put(status_data, "media_ids",  media_ids ))
 +      {:ok, activity} = TwitterAPI.create_status(user, Map.put(status_data, "media_ids",  media_ids))
        conn
        |> json_reply(200, ActivityRepresenter.to_json(activity, %{user: user}))
      else
      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)
  
    def follow(%{assigns: %{user: user}} = conn, params) do
      case TwitterAPI.follow(user, params) do
 -      { :ok, user, followed, _activity } ->
 +      {:ok, user, followed, _activity} ->
          response = followed |> UserRepresenter.to_json(%{for: user})
          conn
          |> json_reply(200, response)
 -      { :error, msg } -> forbidden_json_reply(conn, msg)
 +      {:error, msg} -> forbidden_json_reply(conn, msg)
      end
    end
  
    def unfollow(%{assigns: %{user: user}} = conn, params) do
      case TwitterAPI.unfollow(user, params) do
 -      { :ok, user, unfollowed, } ->
 +      {:ok, user, unfollowed} ->
          response = unfollowed |> UserRepresenter.to_json(%{for: user})
          conn
          |> json_reply(200, response)
 -      { :error, msg } -> forbidden_json_reply(conn, msg)
 +      {:error, msg} -> forbidden_json_reply(conn, msg)
      end
    end
  
 -  def fetch_status(%{assigns: %{user: user}} = conn, %{ "id" => id }) do
 -    response = TwitterAPI.fetch_status(user, id) |> Poison.encode!
 +  def fetch_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
 +    response = Poison.encode!(TwitterAPI.fetch_status(user, id))
  
      conn
      |> json_reply(200, response)
    end
  
 -  def fetch_conversation(%{assigns: %{user: user}} = conn, %{ "id" => id }) do
 +  def fetch_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
      id = String.to_integer(id)
 -    response = TwitterAPI.fetch_conversation(user, id) |> Poison.encode!
 +    response = Poison.encode!(TwitterAPI.fetch_conversation(user, id))
  
      conn
      |> json_reply(200, response)
    def config(conn, _params) do
      response = %{
        site: %{
 -        name: Pleroma.Web.base_url,
 -        server: Pleroma.Web.base_url,
 +        name: Web.base_url,
 +        server: Web.base_url,
          textlimit: -1
        }
      }
  
    def update_avatar(%{assigns: %{user: user}} = conn, params) do
      {:ok, object} = ActivityPub.upload(params)
 -    change = Ecto.Changeset.change(user, %{avatar: object.data})
 +    change = Changeset.change(user, %{avatar: object.data})
      {:ok, user} = Repo.update(change)
  
 -    response = UserRepresenter.to_map(user, %{for: user})
 -    |> Poison.encode!
 +    response = Poison.encode!(UserRepresenter.to_map(user, %{for: user}))
  
      conn
      |> json_reply(200, response)
diff --combined lib/pleroma/web/web.ex
index 19b1ff848590b3a6924d1cce9340d64d8141cd93,ee7ee78e9c1f565cc80d641dee9a31423bfa0e8f..2c343c2d7c6395bfc9ca8f831e088bfbf560cb15
@@@ -20,7 -20,8 +20,7 @@@ defmodule Pleroma.Web d
      quote do
        use Phoenix.Controller, namespace: Pleroma.Web
        import Plug.Conn
 -      import Pleroma.Web.Router.Helpers
 -      import Pleroma.Web.Gettext
 +      import Pleroma.Web.{Gettext, Router.Helpers}
      end
    end
  
@@@ -32,7 -33,9 +32,7 @@@
        # Import convenience functions from controllers
        import Phoenix.Controller, only: [get_csrf_token: 0, get_flash: 2, view_module: 1]
  
 -      import Pleroma.Web.Router.Helpers
 -      import Pleroma.Web.ErrorHelpers
 -      import Pleroma.Web.Gettext
 +      import Pleroma.Web.{ErrorHelpers, Gettext, Router.Helpers}
      end
    end
  
      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,5fa69c2c825b0179eeabd9bad069dcae87056f54..1eb26a89fd19d5b355ac996b4407afee41705b50
  defmodule Pleroma.Web.WebFinger do
-   alias Pleroma.{User, XmlBuilder}
-   alias Pleroma.{Web, Web.OStatus}
 -  alias Pleroma.XmlBuilder
 -  alias Pleroma.{Repo, User}
++
++  alias Pleroma.{Repo, User, XmlBuilder}
++  alias Pleroma.Web
+   alias Pleroma.Web.{XML, Salmon, OStatus}
+   require Logger
  
 -  def host_meta() do
 -    base_url  = Pleroma.Web.base_url
 +  def host_meta do
 +    base_url  = Web.base_url
      {
 -      :XRD, %{ xmlns: "http://docs.oasis-open.org/ns/xri/xrd-1.0" },
 +      :XRD, %{xmlns: "http://docs.oasis-open.org/ns/xri/xrd-1.0"},
        {
 -        :Link, %{ rel: "lrdd", type: "application/xrd+xml", template: "#{base_url}/.well-known/webfinger?resource={uri}"  }
 +        :Link, %{rel: "lrdd", type: "application/xrd+xml", template: "#{base_url}/.well-known/webfinger?resource={uri}"}
        }
      }
      |> XmlBuilder.to_doc
    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,ba86db50e6ede23f17e682783159099531a3e5e4..afbe944c57996e0b08670c7598c7161b338fa1f3
@@@ -1,18 -1,16 +1,20 @@@
  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
  
 -  def verify(subscription, getter \\ &HTTPoison.get/3 ) do
 +  @websub_verifier Application.get_env(:pleroma, :websub_verifier)
 +
 +  def verify(subscription, getter \\ &HTTPoison.get/3) do
      challenge = Base.encode16(:crypto.strong_rand_bytes(8))
 -    lease_seconds = NaiveDateTime.diff(subscription.valid_until, subscription.updated_at) |> to_string
 +    lease_seconds = NaiveDateTime.diff(subscription.valid_until, subscription.updated_at)
 +    lease_seconds = lease_seconds |> to_string
  
      params = %{
        "hub.challenge": challenge,
      with {:ok, response} <- getter.(url, [], [params: params]),
           ^challenge <- response.body
      do
 -      changeset = Ecto.Changeset.change(subscription, %{state: "active"})
 +      changeset = Changeset.change(subscription, %{state: "active"})
        Repo.update(changeset)
      else _e ->
 -      changeset = Ecto.Changeset.change(subscription, %{state: "rejected"})
 -      {:ok, subscription } = Repo.update(changeset)
 +      changeset = Changeset.change(subscription, %{state: "rejected"})
 +      {:ok, subscription} = Repo.update(changeset)
        {:error, subscription}
      end
    end
      where: sub.topic == ^topic and sub.state == "active"
      subscriptions = Repo.all(query)
      Enum.each(subscriptions, fn(sub) ->
 -      response = FeedRepresenter.to_simple_form(user, [activity], [user])
 +      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"},
              {"X-Hub-Signature", "sha1=#{signature}"}
      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),
          callback: callback
        }
  
 -      change = Ecto.Changeset.change(subscription, data)
 +      change = Changeset.change(subscription, data)
        websub = Repo.insert_or_update!(change)
  
 -      change = Ecto.Changeset.change(websub, %{valid_until: NaiveDateTime.add(websub.updated_at, lease_time)})
 +      change = Changeset.change(websub, %{valid_until:
 +                                          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
  
    defp get_subscription(topic, callback) do
 -    Repo.get_by(WebsubServerSubscription, topic: topic, callback: callback) || %WebsubServerSubscription{}
 +    Repo.get_by(WebsubServerSubscription, topic: topic, callback: callback) ||
 +      %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
  
    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