@@ -26,6 +26,12 @@ config :logger, :console, | |||||
format: "$time $metadata[$level] $message\n", | format: "$time $metadata[$level] $message\n", | ||||
metadata: [:request_id] | 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 | # Import environment specific config. This must remain at the bottom | ||||
# of this file so it overrides the configuration defined above. | # of this file so it overrides the configuration defined above. | ||||
import_config "#{Mix.env}.exs" | import_config "#{Mix.env}.exs" |
@@ -24,3 +24,5 @@ config :pleroma, Pleroma.Repo, | |||||
# Reduce hash rounds for testing | # Reduce hash rounds for testing | ||||
config :comeonin, :pbkdf2_rounds, 1 | config :comeonin, :pbkdf2_rounds, 1 | ||||
config :pleroma, :websub_verifier, Pleroma.Web.WebsubMock |
@@ -20,6 +20,13 @@ defmodule Pleroma.User do | |||||
timestamps() | timestamps() | ||||
end | 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 | def ap_id(%User{nickname: nickname}) do | ||||
"#{Pleroma.Web.base_url}/users/#{nickname}" | "#{Pleroma.Web.base_url}/users/#{nickname}" | ||||
end | end | ||||
@@ -57,6 +64,7 @@ defmodule Pleroma.User do | |||||
|> validate_confirmation(:password) | |> validate_confirmation(:password) | ||||
|> unique_constraint(:email) | |> unique_constraint(:email) | ||||
|> unique_constraint(:nickname) | |> unique_constraint(:nickname) | ||||
|> validate_format(:nickname, ~r/^[a-zA-Z\d]+$/) | |||||
if changeset.valid? do | if changeset.valid? do | ||||
hashed = Comeonin.Pbkdf2.hashpwsalt(changeset.changes[:password]) | hashed = Comeonin.Pbkdf2.hashpwsalt(changeset.changes[:password]) | ||||
@@ -9,6 +9,9 @@ defmodule Pleroma.Web.Endpoint do | |||||
# when deploying your static files in production. | # when deploying your static files in production. | ||||
plug Plug.Static, | plug Plug.Static, | ||||
at: "/media", from: "uploads", gzip: false | 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 reloading can be explicitly enabled under the | ||||
# :code_reloader configuration of your endpoint. | # :code_reloader configuration of your endpoint. | ||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -19,6 +19,10 @@ defmodule Pleroma.Web.Router do | |||||
plug Pleroma.Plugs.AuthenticationPlug, %{fetcher: &Pleroma.Web.Router.user_fetcher/1} | plug Pleroma.Plugs.AuthenticationPlug, %{fetcher: &Pleroma.Web.Router.user_fetcher/1} | ||||
end | end | ||||
pipeline :well_known do | |||||
plug :accepts, ["xml", "xrd+xml"] | |||||
end | |||||
scope "/api", Pleroma.Web do | scope "/api", Pleroma.Web do | ||||
pipe_through :api | pipe_through :api | ||||
@@ -61,4 +65,32 @@ defmodule Pleroma.Web.Router do | |||||
post "/qvitter/update_avatar", TwitterAPI.Controller, :update_avatar | post "/qvitter/update_avatar", TwitterAPI.Controller, :update_avatar | ||||
end | 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 | end |
@@ -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 |
@@ -4,11 +4,7 @@ defmodule Pleroma.Web.TwitterAPI.Representers.UserRepresenter do | |||||
alias Pleroma.User | alias Pleroma.User | ||||
def to_map(user, opts) do | 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 | following = if opts[:for] do | ||||
User.following?(opts[:for], user) | User.following?(opts[:for], user) | ||||
else | else | ||||
@@ -66,7 +66,9 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do | |||||
end | end | ||||
with {:ok, activity} <- ActivityPub.insert(activity) do | 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 | ||||
end | end | ||||
@@ -12,11 +12,23 @@ defmodule Pleroma.Web.TwitterAPI.Controller do | |||||
|> json_reply(200, response) | |> json_reply(200, response) | ||||
end | 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 | end | ||||
defp extract_media_ids(status_data) do | 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 | def retweet(%{assigns: %{user: user}} = conn, %{"id" => id}) do | ||||
activity = Repo.get(Activity, id) | 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 | end | ||||
def register(conn, params) do | def register(conn, params) do | ||||
@@ -182,7 +199,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do | |||||
end | end | ||||
defp bad_request_reply(conn, error_message) do | 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) | json_reply(conn, 400, json) | ||||
end | end | ||||
@@ -193,9 +210,11 @@ defmodule Pleroma.Web.TwitterAPI.Controller do | |||||
end | end | ||||
defp forbidden_json_reply(conn, error_message) do | 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) | json_reply(conn, 403, json) | ||||
end | end | ||||
defp error_json(conn, error_message) do | |||||
%{"error" => error_message, "request" => conn.request_path} |> Poison.encode! | |||||
end | |||||
end | end |
@@ -61,12 +61,17 @@ defmodule Pleroma.Web do | |||||
apply(__MODULE__, which, []) | apply(__MODULE__, which, []) | ||||
end | end | ||||
def host do | |||||
settings = Application.get_env(:pleroma, Pleroma.Web.Endpoint) | |||||
settings | |||||
|> Keyword.fetch!(:url) | |||||
|> Keyword.fetch!(:host) | |||||
end | |||||
def base_url do | def base_url do | ||||
settings = Application.get_env(:pleroma, Pleroma.Web.Endpoint) | settings = Application.get_env(:pleroma, Pleroma.Web.Endpoint) | ||||
host = | |||||
settings | |||||
|> Keyword.fetch!(:url) | |||||
|> Keyword.fetch!(:host) | |||||
host = host() | |||||
protocol = settings |> Keyword.fetch!(:protocol) | protocol = settings |> Keyword.fetch!(:protocol) | ||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -39,6 +39,7 @@ defmodule Pleroma.Mixfile do | |||||
{:html_sanitize_ex, "~> 1.0.0"}, | {:html_sanitize_ex, "~> 1.0.0"}, | ||||
{:calendar, "~> 0.16.1"}, | {:calendar, "~> 0.16.1"}, | ||||
{:cachex, "~> 2.1"}, | {:cachex, "~> 2.1"}, | ||||
{:httpoison, "~> 0.11.1"}, | |||||
{:ex_machina, "~> 2.0", only: :test}, | {:ex_machina, "~> 2.0", only: :test}, | ||||
{:mix_test_watch, "~> 0.2", only: :dev}] | {:mix_test_watch, "~> 0.2", only: :dev}] | ||||
end | end | ||||
@@ -18,6 +18,7 @@ | |||||
"gettext": {:hex, :gettext, "0.13.1", "5e0daf4e7636d771c4c71ad5f3f53ba09a9ae5c250e1ab9c42ba9edccc476263", [:mix], []}, | "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]}]}, | "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]}]}, | "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], []}, | "idna": {:hex, :idna, "4.0.0", "10aaa9f79d0b12cf0def53038547855b91144f1bfcc0ec73494f38bb7b9c4961", [:rebar3], []}, | ||||
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], []}, | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], []}, | ||||
"mime": {:hex, :mime, "1.1.0", "01c1d6f4083d8aa5c7b8c246ade95139620ef8effb009edde934e0ec3b28090a", [:mix], []}, | "mime": {:hex, :mime, "1.1.0", "01c1d6f4083d8aa5c7b8c246ade95139620ef8effb009edde934e0ec3b28090a", [:mix], []}, | ||||
@@ -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 |
@@ -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> |
@@ -3,7 +3,7 @@ defmodule Pleroma.Factory do | |||||
def user_factory do | def user_factory do | ||||
user = %Pleroma.User{ | user = %Pleroma.User{ | ||||
name: sequence(:name, &"Test User #{&1}"), | |||||
name: sequence(:name, &"Test テスト User #{&1}"), | |||||
email: sequence(:email, &"user#{&1}@example.com"), | email: sequence(:email, &"user#{&1}@example.com"), | ||||
nickname: sequence(:nickname, &"nick#{&1}"), | nickname: sequence(:nickname, &"nick#{&1}"), | ||||
password_hash: Comeonin.Pbkdf2.hashpwsalt("test"), | password_hash: Comeonin.Pbkdf2.hashpwsalt("test"), | ||||
@@ -81,4 +81,14 @@ defmodule Pleroma.Factory do | |||||
data: data | data: data | ||||
} | } | ||||
end | 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 | end |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -31,10 +31,21 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do | |||||
end | end | ||||
test "with credentials", %{conn: conn, user: user} do | 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}) | assert json_response(conn, 200) == ActivityRepresenter.to_map(Repo.one(Activity), %{user: user}) | ||||
end | end | ||||
end | end | ||||
@@ -139,7 +150,7 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do | |||||
setup [:valid_user] | setup [:valid_user] | ||||
test "without any params", %{conn: conn} do | test "without any params", %{conn: conn} do | ||||
conn = get(conn, "/api/statuses/user_timeline.json") | 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 | end | ||||
test "with user_id", %{conn: conn} do | 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 | test "with credentials", %{conn: conn, user: current_user} do | ||||
note_activity = insert(:note_activity) | 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 | ||||
end | end | ||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |