logo

pleroma

My custom branche(s) on git.pleroma.social/pleroma/pleroma
commit: a9b2ad17596d1b6deca646239a95e94dc644ebf3
parent: 28b203d08fe2e0d7afe3f3ec03a16cef62288b23
Author: dtluna <dtluna@openmailbox.org>
Date:   Fri, 28 Apr 2017 16:06:57 +0300

Merge branch 'develop' of ssh.gitgud.io:lambadalambda/pleroma into feature/unfollow-activity

Diffstat:

Mconfig/config.exs6++++++
Mconfig/test.exs2++
Mlib/pleroma/user.ex8++++++++
Mlib/pleroma/web/endpoint.ex3+++
Alib/pleroma/web/ostatus/activity_representer.ex27+++++++++++++++++++++++++++
Alib/pleroma/web/ostatus/feed_representer.ex31+++++++++++++++++++++++++++++++
Alib/pleroma/web/ostatus/ostatus.ex14++++++++++++++
Alib/pleroma/web/ostatus/ostatus_controller.ex31+++++++++++++++++++++++++++++++
Alib/pleroma/web/ostatus/user_representer.ex20++++++++++++++++++++
Mlib/pleroma/web/router.ex32++++++++++++++++++++++++++++++++
Alib/pleroma/web/salmon/salmon.ex73+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mlib/pleroma/web/twitter_api/representers/user_representer.ex6+-----
Mlib/pleroma/web/twitter_api/twitter_api.ex4+++-
Mlib/pleroma/web/twitter_api/twitter_api_controller.ex45++++++++++++++++++++++++++++++++-------------
Mlib/pleroma/web/web.ex13+++++++++----
Alib/pleroma/web/web_finger/web_finger.ex39+++++++++++++++++++++++++++++++++++++++
Alib/pleroma/web/web_finger/web_finger_controller.ex21+++++++++++++++++++++
Alib/pleroma/web/websub/websub.ex102+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/pleroma/web/websub/websub_controller.ex18++++++++++++++++++
Alib/pleroma/web/websub/websub_server_subscription.ex13+++++++++++++
Alib/xml_builder.ex42++++++++++++++++++++++++++++++++++++++++++
Mmix.exs1+
Mmix.lock1+
Apriv/repo/migrations/20170418200143_create_webssub_server_subscription.exs15+++++++++++++++
Atest/fixtures/salmon.xml3+++
Mtest/support/factory.ex12+++++++++++-
Atest/web/ostatus/activity_representer_test.exs43+++++++++++++++++++++++++++++++++++++++++++
Atest/web/ostatus/feed_representer_test.exs45+++++++++++++++++++++++++++++++++++++++++++++
Atest/web/ostatus/ostatus_controller_test.exs15+++++++++++++++
Atest/web/ostatus/user_representer_test.exs31+++++++++++++++++++++++++++++++
Atest/web/salmon/salmon_test.exs19+++++++++++++++++++
Mtest/web/twitter_api/twitter_api_controller_test.exs37+++++++++++++++++++++++++++++--------
Atest/web/web_finger/web_finger_test.exs11+++++++++++
Atest/web/websub/websub_controller_test.exs23+++++++++++++++++++++++
Atest/web/websub/websub_test.exs90+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/xml_builder_test.exs59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
36 files changed, 923 insertions(+), 32 deletions(-)

diff --git a/config/config.exs b/config/config.exs @@ -26,6 +26,12 @@ config :logger, :console, format: "$time $metadata[$level] $message\n", metadata: [:request_id] +config :mime, :types, %{ + "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 @@ -24,3 +24,5 @@ config :pleroma, Pleroma.Repo, # Reduce hash rounds for testing config :comeonin, :pbkdf2_rounds, 1 + +config :pleroma, :websub_verifier, Pleroma.Web.WebsubMock diff --git 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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/mix.exs b/mix.exs @@ -39,6 +39,7 @@ defmodule Pleroma.Mixfile 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 @@ -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 @@ -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/fixtures/salmon.xml b/test/fixtures/salmon.xml @@ -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-CiA8aWQ-dGFnOmdzLmV4YW1wbGUub3JnOjQwNDAsMjAxNy0wNC0yMzpkaXNmYXZvcjoxOjg6MTk3MC0wMS0wMVQwMDowMDowMCswMDowMDwvaWQ-CiA8dGl0bGU-VW5saWtlPC90aXRsZT4KIDxjb250ZW50IHR5cGU9Imh0bWwiPmxhbWJkYSBubyBsb25nZXIgbGlrZXMgaHR0cDovL3BsZXJvbWEuZXhhbXBsZS5vcmc6NDAwMC9vYmplY3RzL2UyODk2ZmMxLTY1OGItNDJhNy1hMzYyLWUyNThkMzkwNmRlOS48L2NvbnRlbnQ-CiA8YWN0aXZpdHk6dmVyYj5odHRwOi8vYWN0aXZpdHlzdHJlYS5tcy9zY2hlbWEvMS4wL3VuZmF2b3JpdGU8L2FjdGl2aXR5OnZlcmI-CiA8cHVibGlzaGVkPjIwMTctMDQtMjNUMTE6NDc6NTUrMDA6MDA8L3B1Ymxpc2hlZD4KIDx1cGRhdGVkPjIwMTctMDQtMjNUMTE6NDc6NTUrMDA6MDA8L3VwZGF0ZWQ-CiA8YXV0aG9yPgogIDxhY3Rpdml0eTpvYmplY3QtdHlwZT5odHRwOi8vYWN0aXZpdHlzdHJlYS5tcy9zY2hlbWEvMS4wL3BlcnNvbjwvYWN0aXZpdHk6b2JqZWN0LXR5cGU-CiAgPHVyaT5odHRwOi8vZ3MuZXhhbXBsZS5vcmc6NDA0MC9pbmRleC5waHAvdXNlci8xPC91cmk-CiAgPG5hbWU-bGFtYmRhPC9uYW1lPgogIDxsaW5rIHJlbD0iYWx0ZXJuYXRlIiB0eXBlPSJ0ZXh0L2h0bWwiIGhyZWY9Imh0dHA6Ly9ncy5leGFtcGxlLm9yZzo0MDQwL2luZGV4LnBocC9sYW1iZGEiLz4KICA8bGluayByZWw9ImF2YXRhciIgdHlwZT0iaW1hZ2UvcG5nIiBtZWRpYTp3aWR0aD0iOTYiIG1lZGlhOmhlaWdodD0iOTYiIGhyZWY9Imh0dHA6Ly9ncy5leGFtcGxlLm9yZzo0MDQwL3RoZW1lL25lby1nbnUvZGVmYXVsdC1hdmF0YXItcHJvZmlsZS5wbmciLz4KICA8bGluayByZWw9ImF2YXRhciIgdHlwZT0iaW1hZ2UvcG5nIiBtZWRpYTp3aWR0aD0iNDgiIG1lZGlhOmhlaWdodD0iNDgiIGhyZWY9Imh0dHA6Ly9ncy5leGFtcGxlLm9yZzo0MDQwL3RoZW1lL25lby1nbnUvZGVmYXVsdC1hdmF0YXItc3RyZWFtLnBuZyIvPgogIDxsaW5rIHJlbD0iYXZhdGFyIiB0eXBlPSJpbWFnZS9wbmciIG1lZGlhOndpZHRoPSIyNCIgbWVkaWE6aGVpZ2h0PSIyNCIgaHJlZj0iaHR0cDovL2dzLmV4YW1wbGUub3JnOjQwNDAvdGhlbWUvbmVvLWdudS9kZWZhdWx0LWF2YXRhci1taW5pLnBuZyIvPgogIDxwb2NvOnByZWZlcnJlZFVzZXJuYW1lPmxhbWJkYTwvcG9jbzpwcmVmZXJyZWRVc2VybmFtZT4KICA8cG9jbzpkaXNwbGF5TmFtZT5sYW1iZGE8L3BvY286ZGlzcGxheU5hbWU-CiAgPGZvbGxvd2VycyB1cmw9Imh0dHA6Ly9ncy5leGFtcGxlLm9yZzo0MDQwL2luZGV4LnBocC9sYW1iZGEvc3Vic2NyaWJlcnMiPjwvZm9sbG93ZXJzPgogPC9hdXRob3I-CiA8YWN0aXZpdHk6b2JqZWN0PgogIDxhY3Rpdml0eTpvYmplY3QtdHlwZT5odHRwOi8vYWN0aXZpdHlzdHJlYS5tcy9zY2hlbWEvMS4wL25vdGU8L2FjdGl2aXR5Om9iamVjdC10eXBlPgogIDxpZD5odHRwOi8vcGxlcm9tYS5leGFtcGxlLm9yZzo0MDAwL29iamVjdHMvZTI4OTZmYzEtNjU4Yi00MmE3LWEzNjItZTI1OGQzOTA2ZGU5PC9pZD4KICA8dGl0bGU-TmV3IG5vdGUgYnkgbGFpbjI8L3RpdGxlPgogIDxjb250ZW50IHR5cGU9Imh0bWwiPkhlbGxvLjwvY29udGVudD4KICA8bGluayByZWw9ImFsdGVybmF0ZSIgdHlwZT0idGV4dC9odG1sIiBocmVmPSJodHRwOi8vcGxlcm9tYS5leGFtcGxlLm9yZzo0MDAwL29iamVjdHMvZTI4OTZmYzEtNjU4Yi00MmE3LWEzNjItZTI1OGQzOTA2ZGU5Ii8-CiAgPHN0YXR1c19uZXQgbm90aWNlX2lkPSI4Ij48L3N0YXR1c19uZXQ-CiA8L2FjdGl2aXR5Om9iamVjdD4KPC9lbnRyeT4K</me:data><me:encoding>base64url</me:encoding><me:alg>RSA-SHA256</me:alg><me:sig>ZXXHgp_ihTZIJnnFiQuJD0TSvo4OIqrpblHHQQwfpCy-85mtTf0QO1LclX3P3Ra8BqAmhs7j9nDxuEGLuVLTt53DvMP-pOjCtWYDKBbEZQtFIVnCcvBzGPW1HmimdN49M3VtAohbhfVilTrApQpGnI6kHvx7G1fQdQxHRtMsdNI=</me:sig></me:env>+ \ No newline at end of file diff --git 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"), @@ -81,4 +81,14 @@ defmodule Pleroma.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 @@ -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 = """ + <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>#{note_activity.data["object"]["id"]}</id> + <title>New note by #{user.nickname}</title> + <content type="html">#{note_activity.data["object"]["content"]}</content> + <published>#{inserted_at}</published> + <updated>#{updated_at}</updated> + """ + + tuple = ActivityRepresenter.to_simple_form(note_activity, user) + + res = :xmerl.export_simple_content(tuple, :xmerl_xml) |> IO.iodata_to_binary + + assert clean(res) == clean(expected) + end + + test "an unknown activity" do + tuple = ActivityRepresenter.to_simple_form(%Activity{}, nil) + assert is_nil(tuple) + end + + defp clean(string) do + String.replace(string, ~r/\s/, "") + end +end diff --git a/test/web/ostatus/feed_representer_test.exs b/test/web/ostatus/feed_representer_test.exs @@ -0,0 +1,45 @@ +defmodule Pleroma.Web.OStatus.FeedRepresenterTest do + use Pleroma.DataCase + import Pleroma.Factory + alias Pleroma.User + alias Pleroma.Web.OStatus.{FeedRepresenter, UserRepresenter, ActivityRepresenter} + alias Pleroma.Web.OStatus + + test "returns a feed of the last 20 items of the user" do + note_activity = insert(:note_activity) + user = User.get_cached_by_ap_id(note_activity.data["actor"]) + + tuple = FeedRepresenter.to_simple_form(user, [note_activity], [user]) + + most_recent_update = note_activity.updated_at + |> NaiveDateTime.to_iso8601 + + res = :xmerl.export_simple_content(tuple, :xmerl_xml) |> to_string + user_xml = UserRepresenter.to_simple_form(user) + |> :xmerl.export_simple_content(:xmerl_xml) + + entry_xml = ActivityRepresenter.to_simple_form(note_activity, user) + |> :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"> + <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)}" /> + <author> + #{user_xml} + </author> + <entry> + #{entry_xml} + </entry> + </feed> + """ + assert clean(res) == clean(expected) + end + + defp clean(string) do + String.replace(string, ~r/\s/, "") + end +end diff --git a/test/web/ostatus/ostatus_controller_test.exs b/test/web/ostatus/ostatus_controller_test.exs @@ -0,0 +1,15 @@ +defmodule Pleroma.Web.OStatus.OStatusControllerTest do + use Pleroma.Web.ConnCase + import Pleroma.Factory + alias Pleroma.User + + test "gets a feed", %{conn: conn} do + note_activity = insert(:note_activity) + user = User.get_cached_by_ap_id(note_activity.data["actor"]) + + conn = conn + |> get("/users/#{user.nickname}/feed.atom") + + assert response(conn, 200) + end +end diff --git a/test/web/ostatus/user_representer_test.exs b/test/web/ostatus/user_representer_test.exs @@ -0,0 +1,31 @@ +defmodule Pleroma.Web.OStatus.UserRepresenterTest do + use Pleroma.DataCase + alias Pleroma.Web.OStatus.UserRepresenter + + import Pleroma.Factory + alias Pleroma.User + + test "returns a user with id, uri, name and link" do + user = build(:user, nickname: "レイン") + tuple = UserRepresenter.to_simple_form(user) + + res = :xmerl.export_simple_content(tuple, :xmerl_xml) |> to_string + + expected = """ + <id>#{user.ap_id}</id> + <activity:object>http://activitystrea.ms/schema/1.0/person</activity:object> + <uri>#{user.ap_id}</uri> + <poco:preferredUsername>#{user.nickname}</poco:preferredUsername> + <poco:displayName>#{user.name}</poco:displayName> + <poco:note>#{user.bio}</poco:note> + <name>#{user.nickname}</name> + <link rel="avatar" href="#{User.avatar_url(user)}" /> + """ + + assert clean(res) == clean(expected) + end + + defp clean(string) do + String.replace(string, ~r/\s/, "") + end +end diff --git a/test/web/salmon/salmon_test.exs b/test/web/salmon/salmon_test.exs @@ -0,0 +1,19 @@ +defmodule Pleroma.Web.Salmon.SalmonTest do + use Pleroma.DataCase + alias Pleroma.Web.Salmon + + @magickey "RSA.pu0s-halox4tu7wmES1FVSx6u-4wc0YrUFXcqWXZG4-27UmbCOpMQftRCldNRfyA-qLbz-eqiwQhh-1EwUvjsD4cYbAHNGHwTvDOyx5AKthQUP44ykPv7kjKGh3DWKySJvcs9tlUG87hlo7AvnMo9pwRS_Zz2CacQ-MKaXyDepk=.AQAB" + + @wrong_magickey "RSA.pu0s-halox4tu7wmES1FVSx6u-4wc0YrUFXcqWXZG4-27UmbCOpMQftRCldNRfyA-qLbz-eqiwQhh-1EwUvjsD4cYbAHNGHwTvDOyx5AKthQUP44ykPv7kjKGh3DWKySJvcs9tlUG87hlo7AvnMo9pwRS_Zz2CacQ-MKaXyDepk=.AQAA" + + test "decodes a salmon" do + {:ok, salmon} = File.read("test/fixtures/salmon.xml") + {:ok, doc} = Salmon.decode_and_validate(@magickey, salmon) + assert Regex.match?(~r/xml/, doc) + end + + test "errors on wrong magic key" do + {:ok, salmon} = File.read("test/fixtures/salmon.xml") + assert Salmon.decode_and_validate(@wrong_magickey, salmon) == :error + end +end diff --git a/test/web/twitter_api/twitter_api_controller_test.exs b/test/web/twitter_api/twitter_api_controller_test.exs @@ -31,10 +31,21 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do end test "with credentials", %{conn: conn, user: user} do - conn = conn - |> with_credentials(user.nickname, "test") - |> post("/api/statuses/update.json", %{ status: "Nice meme." }) + conn_with_creds = conn |> with_credentials(user.nickname, "test") + request_path = "/api/statuses/update.json" + + error_response = %{"request" => request_path, + "error" => "Client must provide a 'status' parameter with a value."} + conn = conn_with_creds |> post(request_path) + assert json_response(conn, 400) == error_response + + conn = conn_with_creds |> post(request_path, %{ status: "" }) + assert json_response(conn, 400) == error_response + + conn = conn_with_creds |> post(request_path, %{ status: " " }) + assert json_response(conn, 400) == error_response + conn = conn_with_creds |> post(request_path, %{ status: "Nice meme." }) assert json_response(conn, 200) == ActivityRepresenter.to_map(Repo.one(Activity), %{user: user}) end end @@ -139,7 +150,7 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do setup [:valid_user] test "without any params", %{conn: conn} do conn = get(conn, "/api/statuses/user_timeline.json") - assert json_response(conn, 400) == %{"error" => "You need to specify screen_name or user_id"} + assert json_response(conn, 400) == %{"error" => "You need to specify screen_name or user_id", "request" => "/api/statuses/user_timeline.json"} end test "with user_id", %{conn: conn} do @@ -320,11 +331,21 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do test "with credentials", %{conn: conn, user: current_user} do note_activity = insert(:note_activity) - conn = conn - |> with_credentials(current_user.nickname, "test") - |> post("/api/statuses/retweet/#{note_activity.id}.json") + request_path = "/api/statuses/retweet/#{note_activity.id}.json" - assert json_response(conn, 200) + user = Repo.get_by(User, ap_id: note_activity.data["actor"]) + response = conn + |> with_credentials(user.nickname, "test") + |> post(request_path) + assert json_response(response, 400) == %{"error" => "You cannot repeat your own notice.", + "request" => request_path} + + response = conn + |> with_credentials(current_user.nickname, "test") + |> post(request_path) + activity = Repo.get(Activity, note_activity.id) + activity_user = Repo.get_by(User, ap_id: note_activity.data["actor"]) + assert json_response(response, 200) == ActivityRepresenter.to_map(activity, %{user: activity_user, for: current_user}) end end diff --git a/test/web/web_finger/web_finger_test.exs b/test/web/web_finger/web_finger_test.exs @@ -0,0 +1,11 @@ +defmodule Pleroma.Web.WebFingerTest do + use Pleroma.DataCase + + describe "host meta" do + test "returns a link to the xml lrdd" do + host_info = Pleroma.Web.WebFinger.host_meta + + assert String.contains?(host_info, Pleroma.Web.base_url) + end + end +end diff --git a/test/web/websub/websub_controller_test.exs b/test/web/websub/websub_controller_test.exs @@ -0,0 +1,23 @@ +defmodule Pleroma.Web.Websub.WebsubControllerTest do + use Pleroma.Web.ConnCase + import Pleroma.Factory + + test "websub subscription request", %{conn: conn} do + user = insert(:user) + + path = Pleroma.Web.OStatus.pubsub_path(user) + + data = %{ + "hub.callback": "http://example.org/sub", + "hub.mode": "subscribe", + "hub.topic": Pleroma.Web.OStatus.feed_path(user), + "hub.secret": "a random secret", + "hub.lease_seconds": "100" + } + + conn = conn + |> post(path, data) + + assert response(conn, 202) == "Accepted" + end +end diff --git a/test/web/websub/websub_test.exs b/test/web/websub/websub_test.exs @@ -0,0 +1,90 @@ +defmodule Pleroma.Web.WebsubMock do + def verify(sub) 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 + + test "a verification of a request that is accepted" do + sub = insert(:websub_subscription) + topic = sub.topic + + getter = fn (_path, _headers, options) -> + %{ + "hub.challenge": challenge, + "hub.lease_seconds": seconds, + "hub.topic": ^topic, + "hub.mode": "subscribe" + } = Keyword.get(options, :params) + + assert String.to_integer(seconds) > 0 + + {:ok, %HTTPoison.Response{ + status_code: 200, + body: challenge + }} + end + + {:ok, sub} = Websub.verify(sub, getter) + assert sub.state == "active" + end + + test "a verification of a request that doesn't return 200" do + sub = insert(:websub_subscription) + + getter = fn (_path, _headers, _options) -> + {:ok, %HTTPoison.Response{ + status_code: 500, + body: "" + }} + end + + {:error, sub} = Websub.verify(sub, getter) + assert sub.state == "rejected" + end + + test "an incoming subscription request" do + user = insert(:user) + + data = %{ + "hub.callback" => "http://example.org/sub", + "hub.mode" => "subscribe", + "hub.topic" => Pleroma.Web.OStatus.feed_path(user), + "hub.secret" => "a random secret", + "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" + assert subscription.secret == "a random secret" + assert subscription.callback == "http://example.org/sub" + end + + test "an incoming subscription request for an existing subscription" do + user = insert(:user) + sub = insert(:websub_subscription, state: "accepted", topic: Pleroma.Web.OStatus.feed_path(user)) + + data = %{ + "hub.callback" => sub.callback, + "hub.mode" => "subscribe", + "hub.topic" => Pleroma.Web.OStatus.feed_path(user), + "hub.secret" => "a random secret", + "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 + assert subscription.secret == "a random secret" + assert subscription.callback == sub.callback + assert length(Repo.all(WebsubServerSubscription)) == 1 + assert subscription.id == sub.id + end +end diff --git a/test/xml_builder_test.exs b/test/xml_builder_test.exs @@ -0,0 +1,59 @@ +defmodule Pleroma.XmlBuilderTest do + use Pleroma.DataCase + alias Pleroma.XmlBuilder + + test "Build a basic xml string from a tuple" do + data = { :feed, %{ xmlns: "http://www.w3.org/2005/Atom"}, "Some content" } + + expected_xml = "<feed xmlns=\"http://www.w3.org/2005/Atom\">Some content</feed>" + + assert XmlBuilder.to_xml(data) == expected_xml + end + + test "returns a complete document" do + data = { :feed, %{ xmlns: "http://www.w3.org/2005/Atom"}, "Some content" } + + expected_xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><feed xmlns=\"http://www.w3.org/2005/Atom\">Some content</feed>" + + assert XmlBuilder.to_doc(data) == expected_xml + end + + test "Works without attributes" do + data = { + :feed, + "Some content" + } + + expected_xml = "<feed>Some content</feed>" + + assert XmlBuilder.to_xml(data) == expected_xml + end + + test "It works with nested tuples" do + data = { + :feed, + [ + {:guy, "brush"}, + {:lament, %{ configuration: "puzzle" }, "pinhead" } + ] + } + + expected_xml = ~s[<feed><guy>brush</guy><lament configuration="puzzle">pinhead</lament></feed>] + + assert XmlBuilder.to_xml(data) == expected_xml + end + + test "Represents NaiveDateTime as iso8601" do + assert XmlBuilder.to_xml(~N[2000-01-01 13:13:33]) == "2000-01-01T13:13:33" + end + + test "Uses self-closing tags when no content is giving" do + data = { + :link, + %{ rel: "self" } + } + + expected_xml = ~s[<link rel="self" />] + assert XmlBuilder.to_xml(data) == expected_xml + end +end