aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/pleroma/user.ex8
-rw-r--r--lib/pleroma/web/endpoint.ex3
-rw-r--r--lib/pleroma/web/ostatus/activity_representer.ex27
-rw-r--r--lib/pleroma/web/ostatus/feed_representer.ex31
-rw-r--r--lib/pleroma/web/ostatus/ostatus.ex14
-rw-r--r--lib/pleroma/web/ostatus/ostatus_controller.ex31
-rw-r--r--lib/pleroma/web/ostatus/user_representer.ex20
-rw-r--r--lib/pleroma/web/router.ex32
-rw-r--r--lib/pleroma/web/salmon/salmon.ex73
-rw-r--r--lib/pleroma/web/twitter_api/representers/user_representer.ex6
-rw-r--r--lib/pleroma/web/twitter_api/twitter_api.ex4
-rw-r--r--lib/pleroma/web/twitter_api/twitter_api_controller.ex45
-rw-r--r--lib/pleroma/web/web.ex13
-rw-r--r--lib/pleroma/web/web_finger/web_finger.ex39
-rw-r--r--lib/pleroma/web/web_finger/web_finger_controller.ex21
-rw-r--r--lib/pleroma/web/websub/websub.ex102
-rw-r--r--lib/pleroma/web/websub/websub_controller.ex18
-rw-r--r--lib/pleroma/web/websub/websub_server_subscription.ex13
-rw-r--r--lib/xml_builder.ex42
19 files changed, 519 insertions, 23 deletions
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index b260419c7..e1a7befaa 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -20,6 +20,13 @@ defmodule Pleroma.User do
timestamps()
end
+ def avatar_url(user) do
+ case user.avatar do
+ %{"url" => [%{"href" => href} | _]} -> href
+ _ -> "https://placehold.it/48x48"
+ end
+ end
+
def ap_id(%User{nickname: nickname}) do
"#{Pleroma.Web.base_url}/users/#{nickname}"
end
@@ -57,6 +64,7 @@ defmodule Pleroma.User do
|> validate_confirmation(:password)
|> unique_constraint(:email)
|> unique_constraint(:nickname)
+ |> validate_format(:nickname, ~r/^[a-zA-Z\d]+$/)
if changeset.valid? do
hashed = Comeonin.Pbkdf2.hashpwsalt(changeset.changes[:password])
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..14ac3ebf4
--- /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) || user).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
new file mode 100644
index 000000000..65dfc5643
--- /dev/null
+++ b/lib/pleroma/web/ostatus/user_representer.ex
@@ -0,0 +1,20 @@
+defmodule Pleroma.Web.OStatus.UserRepresenter do
+ alias Pleroma.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 6a2b37aec..a4f13c879 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -19,6 +19,10 @@ defmodule Pleroma.Web.Router do
plug Pleroma.Plugs.AuthenticationPlug, %{fetcher: &Pleroma.Web.Router.user_fetcher/1}
end
+ pipeline :well_known do
+ plug :accepts, ["xml", "xrd+xml"]
+ end
+
scope "/api", Pleroma.Web do
pipe_through :api
@@ -61,4 +65,32 @@ defmodule Pleroma.Web.Router 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/salmon/salmon.ex b/lib/pleroma/web/salmon/salmon.ex
new file mode 100644
index 000000000..3881f2758
--- /dev/null
+++ b/lib/pleroma/web/salmon/salmon.ex
@@ -0,0 +1,73 @@
+defmodule Pleroma.Web.Salmon do
+ use Bitwise
+
+ def decode(salmon) do
+ {doc, _rest} = :xmerl_scan.string(to_charlist(salmon))
+
+ {:xmlObj, :string, data} = :xmerl_xpath.string('string(//me:data[1])', doc)
+ {:xmlObj, :string, sig} = :xmerl_xpath.string('string(//me:sig[1])', doc)
+ {:xmlObj, :string, alg} = :xmerl_xpath.string('string(//me:alg[1])', doc)
+ {: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)
+ encoding = to_string(encoding)
+ type = to_string(type)
+
+ [data, type, encoding, alg, sig]
+ 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
+ end
+
+ def decode_and_validate(magickey, salmon) do
+ [data, type, encoding, alg, sig] = decode(salmon)
+
+ signed_text = [data, type, encoding, alg]
+ |> Enum.map(&Base.url_encode64/1)
+ |> Enum.join(".")
+
+ key = decode_key(magickey)
+
+ verify = :public_key.verify(signed_text, :sha256, sig, key)
+
+ if verify do
+ {:ok, data}
+ else
+ :error
+ end
+ end
+
+ defp 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)
+ end
+
+ [modulus, exponent] = magickey
+ |> String.split(".")
+ |> Enum.map(&Base.url_decode64!/1)
+ |> Enum.map(make_integer)
+
+ {:RSAPublicKey, modulus, exponent}
+ end
+end
diff --git a/lib/pleroma/web/twitter_api/representers/user_representer.ex b/lib/pleroma/web/twitter_api/representers/user_representer.ex
index f358baf3c..ab7d6d353 100644
--- a/lib/pleroma/web/twitter_api/representers/user_representer.ex
+++ b/lib/pleroma/web/twitter_api/representers/user_representer.ex
@@ -4,11 +4,7 @@ defmodule Pleroma.Web.TwitterAPI.Representers.UserRepresenter do
alias Pleroma.User
def to_map(user, opts) do
- image = case user.avatar do
- %{"url" => [%{"href" => href} | _]} -> href
- _ -> "https://placehold.it/48x48"
- end
-
+ image = User.avatar_url(user)
following = if opts[:for] do
User.following?(opts[:for], user)
else
diff --git a/lib/pleroma/web/twitter_api/twitter_api.ex b/lib/pleroma/web/twitter_api/twitter_api.ex
index 0f39ed7f3..8e2cd98ca 100644
--- a/lib/pleroma/web/twitter_api/twitter_api.ex
+++ b/lib/pleroma/web/twitter_api/twitter_api.ex
@@ -66,7 +66,9 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI 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/twitter_api/twitter_api_controller.ex b/lib/pleroma/web/twitter_api/twitter_api_controller.ex
index 4d21ce9d3..b5b829ca0 100644
--- a/lib/pleroma/web/twitter_api/twitter_api_controller.ex
+++ b/lib/pleroma/web/twitter_api/twitter_api_controller.ex
@@ -12,11 +12,23 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
|> json_reply(200, response)
end
- def status_update(%{assigns: %{user: user}} = conn, status_data) do
- media_ids = extract_media_ids(status_data)
- {:ok, activity} = TwitterAPI.create_status(user, Map.put(status_data, "media_ids", media_ids ))
- conn
- |> json_reply(200, ActivityRepresenter.to_json(activity, %{user: user}))
+ 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 ))
+ conn
+ |> json_reply(200, ActivityRepresenter.to_json(activity, %{user: user}))
+ else
+ empty_status_reply(conn)
+ end
+ end
+
+ def status_update(conn, _status_data) do
+ empty_status_reply(conn)
+ end
+
+ defp empty_status_reply(conn) do
+ bad_request_reply(conn, "Client must provide a 'status' parameter with a value.")
end
defp extract_media_ids(status_data) do
@@ -151,11 +163,16 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
def retweet(%{assigns: %{user: user}} = conn, %{"id" => id}) do
activity = Repo.get(Activity, id)
- {:ok, status} = TwitterAPI.retweet(user, activity)
- response = Poison.encode!(status)
+ if activity.data["actor"] == user.ap_id do
+ bad_request_reply(conn, "You cannot repeat your own notice.")
+ else
+ {:ok, status} = TwitterAPI.retweet(user, activity)
+ response = Poison.encode!(status)
- conn
- |> json_reply(200, response)
+ conn
+
+ |> json_reply(200, response)
+ end
end
def register(conn, params) do
@@ -182,7 +199,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
end
defp bad_request_reply(conn, error_message) do
- json = Poison.encode!(%{"error" => error_message})
+ json = error_json(conn, error_message)
json_reply(conn, 400, json)
end
@@ -193,9 +210,11 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
end
defp forbidden_json_reply(conn, error_message) do
- json = %{"error" => error_message, "request" => conn.request_path}
- |> Poison.encode!
-
+ json = error_json(conn, error_message)
json_reply(conn, 403, json)
end
+
+ defp error_json(conn, error_message) do
+ %{"error" => error_message, "request" => conn.request_path} |> Poison.encode!
+ end
end
diff --git a/lib/pleroma/web/web.ex b/lib/pleroma/web/web.ex
index d03db2231..a81e3e6e1 100644
--- a/lib/pleroma/web/web.ex
+++ b/lib/pleroma/web/web.ex
@@ -61,12 +61,17 @@ 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 =
- settings
- |> Keyword.fetch!(:url)
- |> Keyword.fetch!(:host)
+
+ host = host()
protocol = settings |> Keyword.fetch!(:protocol)
diff --git a/lib/pleroma/web/web_finger/web_finger.ex b/lib/pleroma/web/web_finger/web_finger.ex
new file mode 100644
index 000000000..eb540e92a
--- /dev/null
+++ b/lib/pleroma/web/web_finger/web_finger.ex
@@ -0,0 +1,39 @@
+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
+ {
+ :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}" }
+ }
+ }
+ |> XmlBuilder.to_doc
+ end
+
+ def webfinger(resource) do
+ host = Pleroma.Web.host
+ regex = ~r/acct:(?<username>\w+)@#{host}/
+ case Regex.named_captures(regex, resource) do
+ %{"username" => username} ->
+ user = User.get_cached_by_nickname(username)
+ {:ok, represent_user(user)}
+ _ -> nil
+ end
+ end
+
+ def represent_user(user) do
+ {
+ :XRD, %{xmlns: "http://docs.oasis-open.org/ns/xri/xrd-1.0"},
+ [
+ {:Subject, "acct:#{user.nickname}@#{Pleroma.Web.host}"},
+ {:Alias, user.ap_id},
+ {:Link, %{rel: "http://schemas.google.com/g/2010#updates-from", type: "application/atom+xml", href: OStatus.feed_path(user)}}
+ ]
+ }
+ |> XmlBuilder.to_doc
+ end
+end
diff --git a/lib/pleroma/web/web_finger/web_finger_controller.ex b/lib/pleroma/web/web_finger/web_finger_controller.ex
new file mode 100644
index 000000000..7c0fd3142
--- /dev/null
+++ b/lib/pleroma/web/web_finger/web_finger_controller.ex
@@ -0,0 +1,21 @@
+defmodule Pleroma.Web.WebFinger.WebFingerController do
+ use Pleroma.Web, :controller
+
+ alias Pleroma.Web.WebFinger
+
+ def host_meta(conn, _params) do
+ xml = WebFinger.host_meta
+
+ conn
+ |> put_resp_content_type("application/xrd+xml")
+ |> send_resp(200, xml)
+ end
+
+ def webfinger(conn, %{"resource" => resource}) do
+ {:ok, response} = Pleroma.Web.WebFinger.webfinger(resource)
+
+ conn
+ |> put_resp_content_type("application/xrd+xml")
+ |> send_resp(200, response)
+ end
+end
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/lib/xml_builder.ex b/lib/xml_builder.ex
new file mode 100644
index 000000000..ac1ac8a74
--- /dev/null
+++ b/lib/xml_builder.ex
@@ -0,0 +1,42 @@
+defmodule Pleroma.XmlBuilder do
+ def to_xml({tag, attributes, content}) do
+ open_tag = make_open_tag(tag, attributes)
+
+ content_xml = to_xml(content)
+
+ "<#{open_tag}>#{content_xml}</#{tag}>"
+ end
+
+ def to_xml({tag, %{} = attributes}) do
+ open_tag = make_open_tag(tag, attributes)
+
+ "<#{open_tag} />"
+ end
+
+ def to_xml({tag, content}), do: to_xml({tag, %{}, content})
+
+ def to_xml(content) when is_binary(content) do
+ to_string(content)
+ end
+
+ def to_xml(content) when is_list(content) do
+ for element <- content do
+ to_xml(element)
+ end
+ |> Enum.join
+ end
+
+ def to_xml(%NaiveDateTime{} = time) do
+ NaiveDateTime.to_iso8601(time)
+ end
+
+ def to_doc(content), do: "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" <> to_xml(content)
+
+ defp make_open_tag(tag, attributes) do
+ attributes_string = for {attribute, value} <- attributes do
+ "#{attribute}=\"#{value}\""
+ end |> Enum.join(" ")
+
+ Enum.join([tag, attributes_string], " ") |> String.strip
+ end
+end