diff --git a/config/config.exs b/config/config.exs
index 18a2490a4..3826dddff 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -30,6 +30,8 @@
"application/xrd+xml" => ["xrd+xml"]
}
+config :pleroma, :websub_verifier, Pleroma.Web.Websub
+
# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{Mix.env}.exs"
diff --git a/config/test.exs b/config/test.exs
index f5d6f240d..5d91279a2 100644
--- a/config/test.exs
+++ b/config/test.exs
@@ -24,3 +24,5 @@
# Reduce hash rounds for testing
config :comeonin, :pbkdf2_rounds, 1
+
+config :pleroma, :websub_verifier, Pleroma.Web.WebsubMock
diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex
index 6af42a685..45a3a345d 100644
--- a/lib/pleroma/web/endpoint.ex
+++ b/lib/pleroma/web/endpoint.ex
@@ -9,6 +9,9 @@ defmodule Pleroma.Web.Endpoint do
# when deploying your static files in production.
plug Plug.Static,
at: "/media", from: "uploads", gzip: false
+ plug Plug.Static,
+ at: "/", from: :pleroma,
+ only: ~w(index.html static)
# Code reloading can be explicitly enabled under the
# :code_reloader configuration of your endpoint.
diff --git a/lib/pleroma/web/ostatus/activity_representer.ex b/lib/pleroma/web/ostatus/activity_representer.ex
new file mode 100644
index 000000000..590abc8bb
--- /dev/null
+++ b/lib/pleroma/web/ostatus/activity_representer.ex
@@ -0,0 +1,27 @@
+defmodule Pleroma.Web.OStatus.ActivityRepresenter do
+ def to_simple_form(%{data: %{"object" => %{"type" => "Note"}}} = activity, user) do
+ h = fn(str) -> [to_charlist(str)] end
+
+ updated_at = activity.updated_at
+ |> NaiveDateTime.to_iso8601
+ inserted_at = activity.inserted_at
+ |> NaiveDateTime.to_iso8601
+
+ attachments = Enum.map(activity.data["object"]["attachment"] || [], fn(attachment) ->
+ url = hd(attachment["url"])
+ {:link, [rel: 'enclosure', href: to_charlist(url["href"]), type: to_charlist(url["mediaType"])], []}
+ end)
+
+ [
+ {:"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"])},
+ {:title, ['New note by #{user.nickname}']},
+ {:content, [type: 'html'], h.(activity.data["object"]["content"])},
+ {:published, h.(inserted_at)},
+ {:updated, h.(updated_at)}
+ ] ++ attachments
+ end
+
+ def to_simple_form(_,_), do: nil
+end
diff --git a/lib/pleroma/web/ostatus/feed_representer.ex b/lib/pleroma/web/ostatus/feed_representer.ex
new file mode 100644
index 000000000..749cb10d0
--- /dev/null
+++ b/lib/pleroma/web/ostatus/feed_representer.ex
@@ -0,0 +1,31 @@
+defmodule Pleroma.Web.OStatus.FeedRepresenter do
+ alias Pleroma.Web.OStatus
+ alias Pleroma.Web.OStatus.{UserRepresenter, ActivityRepresenter}
+
+ def to_simple_form(user, activities, users) do
+ most_recent_update = List.first(activities).updated_at
+ |> NaiveDateTime.to_iso8601
+
+ h = fn(str) -> [to_charlist(str)] end
+
+ entries = Enum.map(activities, 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:activity": 'http://activitystrea.ms/spec/1.0/',
+ "xmlns:poco": 'http://portablecontacts.net/spec/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))], []},
+ {:author, UserRepresenter.to_simple_form(user)},
+ ] ++ entries
+ }]
+ end
+end
diff --git a/lib/pleroma/web/ostatus/ostatus.ex b/lib/pleroma/web/ostatus/ostatus.ex
new file mode 100644
index 000000000..d21b9078f
--- /dev/null
+++ b/lib/pleroma/web/ostatus/ostatus.ex
@@ -0,0 +1,14 @@
+defmodule Pleroma.Web.OStatus do
+ alias Pleroma.Web
+
+ def feed_path(user) do
+ "#{user.ap_id}/feed.atom"
+ end
+
+ def pubsub_path(user) do
+ "#{Web.base_url}/push/hub/#{user.nickname}"
+ end
+
+ def user_path(user) do
+ end
+end
diff --git a/lib/pleroma/web/ostatus/ostatus_controller.ex b/lib/pleroma/web/ostatus/ostatus_controller.ex
new file mode 100644
index 000000000..3c8d8c0f1
--- /dev/null
+++ b/lib/pleroma/web/ostatus/ostatus_controller.ex
@@ -0,0 +1,31 @@
+defmodule Pleroma.Web.OStatus.OStatusController do
+ use Pleroma.Web, :controller
+
+ alias Pleroma.{User, Activity}
+ alias Pleroma.Web.OStatus.FeedRepresenter
+ alias Pleroma.Repo
+ import Ecto.Query
+
+ def feed(conn, %{"nickname" => nickname}) do
+ user = User.get_cached_by_nickname(nickname)
+ query = from activity in Activity,
+ where: fragment("? @> ?", activity.data, ^%{actor: user.ap_id}),
+ limit: 20,
+ order_by: [desc: :inserted_at]
+
+ activities = query
+ |> Repo.all
+
+ response = FeedRepresenter.to_simple_form(user, activities, [user])
+ |> :xmerl.export_simple(:xmerl_xml)
+ |> to_string
+
+ conn
+ |> put_resp_content_type("application/atom+xml")
+ |> send_resp(200, response)
+ end
+
+ def temp(conn, params) do
+ IO.inspect(params)
+ end
+end
diff --git a/lib/pleroma/web/ostatus/user_representer.ex b/lib/pleroma/web/ostatus/user_representer.ex
index 66fc6e053..65dfc5643 100644
--- a/lib/pleroma/web/ostatus/user_representer.ex
+++ b/lib/pleroma/web/ostatus/user_representer.ex
@@ -1,14 +1,20 @@
defmodule Pleroma.Web.OStatus.UserRepresenter do
alias Pleroma.User
- def to_tuple(user, wrapper \\ :author) do
- {
- wrapper, [
- { :id, user.ap_id },
- { :"activity:object", "http://activitystrea.ms/schema/1.0/person" },
- { :uri, user.ap_id },
- { :name, user.nickname },
- { :link, %{rel: "avatar", href: User.avatar_url(user)}}
- ]
- }
+ def to_simple_form(user) do
+ ap_id = to_charlist(user.ap_id)
+ nickname = to_charlist(user.nickname)
+ name = to_charlist(user.name)
+ bio = to_charlist(user.bio)
+ avatar_url = to_charlist(User.avatar_url(user))
+ [
+ { :id, [ap_id] },
+ { :"activity:object", ['http://activitystrea.ms/schema/1.0/person'] },
+ { :uri, [ap_id] },
+ { :"poco:preferredUsername", [nickname] },
+ { :"poco:displayName", [name] },
+ { :"poco:note", [bio] },
+ { :name, [nickname] },
+ { :link, [rel: 'avatar', href: avatar_url], []}
+ ]
end
end
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index c2cec1d85..a4f13c879 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -66,10 +66,31 @@ def user_fetcher(username) do
post "/qvitter/update_avatar", TwitterAPI.Controller, :update_avatar
end
+ pipeline :ostatus do
+ plug :accepts, ["xml", "atom"]
+ end
+
+ scope "/", Pleroma.Web do
+ pipe_through :ostatus
+
+ get "/users/:nickname/feed", OStatus.OStatusController, :feed
+ post "/push/hub/:nickname", Websub.WebsubController, :websub_subscription_request
+ end
+
scope "/.well-known", Pleroma.Web do
pipe_through :well_known
get "/host-meta", WebFinger.WebFingerController, :host_meta
get "/webfinger", WebFinger.WebFingerController, :webfinger
end
+
+ scope "/", Fallback do
+ get "/*path", RedirectController, :redirector
+ end
+
+end
+
+defmodule Fallback.RedirectController do
+ use Pleroma.Web, :controller
+ def redirector(conn, _params), do: send_file(conn, 200, "priv/static/index.html")
end
diff --git a/lib/pleroma/web/twitter_api/twitter_api.ex b/lib/pleroma/web/twitter_api/twitter_api.ex
index 1053120c4..0f84cffbd 100644
--- a/lib/pleroma/web/twitter_api/twitter_api.ex
+++ b/lib/pleroma/web/twitter_api/twitter_api.ex
@@ -66,7 +66,9 @@ def create_status(user = %User{}, data = %{}) do
end
with {:ok, activity} <- ActivityPub.insert(activity) do
- add_conversation_id(activity)
+ {:ok, activity} = add_conversation_id(activity)
+ Pleroma.Web.Websub.publish(Pleroma.Web.OStatus.feed_path(user), user, activity)
+ {:ok, activity}
end
end
diff --git a/lib/pleroma/web/web_finger/web_finger.ex b/lib/pleroma/web/web_finger/web_finger.ex
index 258ff7671..eb540e92a 100644
--- a/lib/pleroma/web/web_finger/web_finger.ex
+++ b/lib/pleroma/web/web_finger/web_finger.ex
@@ -1,6 +1,7 @@
defmodule Pleroma.Web.WebFinger do
alias Pleroma.XmlBuilder
alias Pleroma.User
+ alias Pleroma.Web.OStatus
def host_meta() do
base_url = Pleroma.Web.base_url
@@ -30,7 +31,7 @@ def represent_user(user) do
[
{:Subject, "acct:#{user.nickname}@#{Pleroma.Web.host}"},
{:Alias, user.ap_id},
- {:Link, %{rel: "http://schemas.google.com/g/2010#updates-from", type: "application/atom+xml", href: "#{user.ap_id}.atom"}}
+ {:Link, %{rel: "http://schemas.google.com/g/2010#updates-from", type: "application/atom+xml", href: OStatus.feed_path(user)}}
]
}
|> XmlBuilder.to_doc
diff --git a/lib/pleroma/web/websub/websub.ex b/lib/pleroma/web/websub/websub.ex
new file mode 100644
index 000000000..cc66b52dd
--- /dev/null
+++ b/lib/pleroma/web/websub/websub.ex
@@ -0,0 +1,102 @@
+defmodule Pleroma.Web.Websub do
+ alias Pleroma.Repo
+ alias Pleroma.Web.Websub.WebsubServerSubscription
+ alias Pleroma.Web.OStatus.FeedRepresenter
+ alias Pleroma.Web.OStatus
+
+ import Ecto.Query
+
+ @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
+
+ params = %{
+ "hub.challenge": challenge,
+ "hub.lease_seconds": lease_seconds,
+ "hub.topic": subscription.topic,
+ "hub.mode": "subscribe"
+ }
+
+ url = hd(String.split(subscription.callback, "?"))
+ query = URI.parse(subscription.callback).query || ""
+ params = Map.merge(params, URI.decode_query(query))
+ with {:ok, response} <- getter.(url, [], [params: params]),
+ ^challenge <- response.body
+ do
+ changeset = Ecto.Changeset.change(subscription, %{state: "active"})
+ Repo.update(changeset)
+ else _e ->
+ changeset = Ecto.Changeset.change(subscription, %{state: "rejected"})
+ {:ok, subscription } = Repo.update(changeset)
+ {:error, subscription}
+ end
+ end
+
+ def publish(topic, user, activity) do
+ query = from sub in WebsubServerSubscription,
+ 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])
+ |> :xmerl.export_simple(:xmerl_xml)
+
+ signature = :crypto.hmac(:sha, sub.secret, response) |> Base.encode16
+
+ HTTPoison.post(sub.callback, response, [
+ {"Content-Type", "application/atom+xml"},
+ {"X-Hub-Signature", "sha1=#{signature}"}
+ ])
+ end)
+ end
+
+ def incoming_subscription_request(user, %{"hub.mode" => "subscribe"} = params) do
+ with {:ok, topic} <- valid_topic(params, user),
+ {:ok, lease_time} <- lease_time(params),
+ secret <- params["hub.secret"],
+ callback <- params["hub.callback"]
+ do
+ subscription = get_subscription(topic, callback)
+ data = %{
+ state: subscription.state || "requested",
+ topic: topic,
+ secret: secret,
+ callback: callback
+ }
+
+ change = Ecto.Changeset.change(subscription, data)
+ websub = Repo.insert_or_update!(change)
+
+ change = Ecto.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)
+
+ {:ok, websub}
+ else {:error, reason} ->
+ {:error, reason}
+ end
+ end
+
+ defp get_subscription(topic, callback) do
+ Repo.get_by(WebsubServerSubscription, topic: topic, callback: callback) || %WebsubServerSubscription{}
+ end
+
+ defp lease_time(%{"hub.lease_seconds" => lease_seconds}) do
+ {:ok, String.to_integer(lease_seconds)}
+ end
+
+ defp lease_time(_) do
+ {:ok, 60 * 60 * 24 * 3} # three days
+ end
+
+ defp valid_topic(%{"hub.topic" => topic}, user) do
+ if topic == OStatus.feed_path(user) do
+ {:ok, topic}
+ else
+ {:error, "Wrong topic requested, expected #{OStatus.feed_path(user)}, got #{topic}"}
+ end
+ end
+end
diff --git a/lib/pleroma/web/websub/websub_controller.ex b/lib/pleroma/web/websub/websub_controller.ex
new file mode 100644
index 000000000..5d54c6ef5
--- /dev/null
+++ b/lib/pleroma/web/websub/websub_controller.ex
@@ -0,0 +1,18 @@
+defmodule Pleroma.Web.Websub.WebsubController do
+ use Pleroma.Web, :controller
+ alias Pleroma.User
+ alias Pleroma.Web.Websub
+
+ def websub_subscription_request(conn, %{"nickname" => nickname} = params) do
+ user = User.get_cached_by_nickname(nickname)
+
+ with {:ok, _websub} <- Websub.incoming_subscription_request(user, params)
+ do
+ conn
+ |> send_resp(202, "Accepted")
+ else {:error, reason} ->
+ conn
+ |> send_resp(500, reason)
+ end
+ end
+end
diff --git a/lib/pleroma/web/websub/websub_server_subscription.ex b/lib/pleroma/web/websub/websub_server_subscription.ex
new file mode 100644
index 000000000..a29dd5860
--- /dev/null
+++ b/lib/pleroma/web/websub/websub_server_subscription.ex
@@ -0,0 +1,13 @@
+defmodule Pleroma.Web.Websub.WebsubServerSubscription do
+ use Ecto.Schema
+
+ schema "websub_server_subscriptions" do
+ field :topic, :string
+ field :callback, :string
+ field :secret, :string
+ field :valid_until, :naive_datetime
+ field :state, :string
+
+ timestamps()
+ end
+end
diff --git a/mix.exs b/mix.exs
index f6831550b..0e54f0246 100644
--- a/mix.exs
+++ b/mix.exs
@@ -39,6 +39,7 @@ defp deps do
{:html_sanitize_ex, "~> 1.0.0"},
{:calendar, "~> 0.16.1"},
{:cachex, "~> 2.1"},
+ {:httpoison, "~> 0.11.1"},
{:ex_machina, "~> 2.0", only: :test},
{:mix_test_watch, "~> 0.2", only: :dev}]
end
diff --git a/mix.lock b/mix.lock
index a44ffa8d0..225a62f7a 100644
--- a/mix.lock
+++ b/mix.lock
@@ -18,6 +18,7 @@
"gettext": {:hex, :gettext, "0.13.1", "5e0daf4e7636d771c4c71ad5f3f53ba09a9ae5c250e1ab9c42ba9edccc476263", [:mix], []},
"hackney": {:hex, :hackney, "1.7.1", "e238c52c5df3c3b16ce613d3a51c7220a784d734879b1e231c9babd433ac1cb4", [:rebar3], [{:certifi, "1.0.0", [hex: :certifi, optional: false]}, {:idna, "4.0.0", [hex: :idna, optional: false]}, {:metrics, "1.0.1", [hex: :metrics, optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, optional: false]}]},
"html_sanitize_ex": {:hex, :html_sanitize_ex, "1.0.1", "2572e7122c78ab7e57b613e7c7f5e42bf9b3c25e430e32f23f1413d86db8a0af", [:mix], [{:mochiweb, "~> 2.12.2", [hex: :mochiweb, optional: false]}]},
+ "httpoison": {:hex, :httpoison, "0.11.1", "d06c571274c0e77b6cc50e548db3fd7779f611fbed6681fd60a331f66c143a0b", [:mix], [{:hackney, "~> 1.7.0", [hex: :hackney, optional: false]}]},
"idna": {:hex, :idna, "4.0.0", "10aaa9f79d0b12cf0def53038547855b91144f1bfcc0ec73494f38bb7b9c4961", [:rebar3], []},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], []},
"mime": {:hex, :mime, "1.1.0", "01c1d6f4083d8aa5c7b8c246ade95139620ef8effb009edde934e0ec3b28090a", [:mix], []},
diff --git a/priv/repo/migrations/20170418200143_create_webssub_server_subscription.exs b/priv/repo/migrations/20170418200143_create_webssub_server_subscription.exs
new file mode 100644
index 000000000..fe2fa2304
--- /dev/null
+++ b/priv/repo/migrations/20170418200143_create_webssub_server_subscription.exs
@@ -0,0 +1,15 @@
+defmodule Pleroma.Repo.Migrations.CreateWebsubServerSubscription do
+ use Ecto.Migration
+
+ def change do
+ create table(:websub_server_subscriptions) do
+ add :topic, :string
+ add :callback, :string
+ add :secret, :string
+ add :valid_until, :naive_datetime
+ add :state, :string
+
+ timestamps()
+ end
+ end
+end
diff --git a/test/support/factory.ex b/test/support/factory.ex
index 3fc9cf710..d7c16f0e0 100644
--- a/test/support/factory.ex
+++ b/test/support/factory.ex
@@ -3,7 +3,7 @@ defmodule Pleroma.Factory do
def user_factory do
user = %Pleroma.User{
- name: sequence(:name, &"Test User #{&1}"),
+ name: sequence(:name, &"Test テスト User #{&1}"),
email: sequence(:email, &"user#{&1}@example.com"),
nickname: sequence(:nickname, &"nick#{&1}"),
password_hash: Comeonin.Pbkdf2.hashpwsalt("test"),
@@ -64,4 +64,14 @@ def like_activity_factory do
data: data
}
end
+
+ def websub_subscription_factory do
+ %Pleroma.Web.Websub.WebsubServerSubscription{
+ topic: "http://example.org",
+ callback: "http://example/org/callback",
+ secret: "here's a secret",
+ valid_until: NaiveDateTime.add(NaiveDateTime.utc_now, 100),
+ state: "requested"
+ }
+ end
end
diff --git a/test/web/ostatus/activity_representer_test.exs b/test/web/ostatus/activity_representer_test.exs
new file mode 100644
index 000000000..61df41a1d
--- /dev/null
+++ b/test/web/ostatus/activity_representer_test.exs
@@ -0,0 +1,43 @@
+defmodule Pleroma.Web.OStatus.ActivityRepresenterTest do
+ use Pleroma.DataCase
+
+ alias Pleroma.Web.OStatus.ActivityRepresenter
+ alias Pleroma.{User, Activity}
+
+ import Pleroma.Factory
+
+ test "a note activity" do
+ note_activity = insert(:note_activity)
+ updated_at = note_activity.updated_at
+ |> NaiveDateTime.to_iso8601
+ inserted_at = note_activity.inserted_at
+ |> NaiveDateTime.to_iso8601
+
+ user = User.get_cached_by_ap_id(note_activity.data["actor"])
+
+ expected = """
+