Return salmon path for users, basic incoming salmon handling.

This commit is contained in:
Roger Braun 2017-04-24 18:46:34 +02:00
parent 34d3aea92f
commit ab0114fbaa
9 changed files with 272 additions and 39 deletions

View file

@ -19,6 +19,48 @@ def insert(map) when is_map(map) do
Repo.insert(%Activity{data: map}) Repo.insert(%Activity{data: map})
end end
def create(to, actor, context, object, additional \\ %{}, published \\ nil) do
published = published || make_date()
activity = %{
"type" => "Create",
"to" => to,
"actor" => actor.ap_id,
"object" => object,
"published" => published,
"context" => context
}
|> Map.merge(additional)
with {:ok, activity} <- insert(activity) do
{:ok, activity} = add_conversation_id(activity)
if actor.local do
Pleroma.Web.Websub.publish(Pleroma.Web.OStatus.feed_path(actor), actor, activity)
end
{:ok, activity}
end
end
defp add_conversation_id(activity) do
if is_integer(activity.data["statusnetConversationId"]) do
{:ok, activity}
else
data = activity.data
|> put_in(["object", "statusnetConversationId"], activity.id)
|> put_in(["statusnetConversationId"], activity.id)
object = Object.get_by_ap_id(activity.data["object"]["id"])
changeset = Ecto.Changeset.change(object, data: data["object"])
Repo.update(changeset)
changeset = Ecto.Changeset.change(activity, data: data)
Repo.update(changeset)
end
end
def like(%User{ap_id: ap_id} = user, %Object{data: %{ "id" => id}} = object) do def like(%User{ap_id: ap_id} = user, %Object{data: %{ "id" => id}} = object) do
cond do cond do
# There's already a like here, so return the original activity. # There's already a like here, so return the original activity.

View file

@ -23,6 +23,7 @@ def to_simple_form(user, activities, users) do
{:title, ['#{user.nickname}\'s timeline']}, {:title, ['#{user.nickname}\'s timeline']},
{:updated, h.(most_recent_update)}, {:updated, h.(most_recent_update)},
{:link, [rel: 'hub', href: h.(OStatus.pubsub_path(user))], []}, {:link, [rel: 'hub', href: h.(OStatus.pubsub_path(user))], []},
{:link, [rel: 'salmon', href: h.(OStatus.salmon_path(user))], []},
{:link, [rel: 'self', href: h.(OStatus.feed_path(user))], []}, {:link, [rel: 'self', href: h.(OStatus.feed_path(user))], []},
{:author, UserRepresenter.to_simple_form(user)}, {:author, UserRepresenter.to_simple_form(user)},
] ++ entries ] ++ entries

View file

@ -1,5 +1,9 @@
defmodule Pleroma.Web.OStatus do defmodule Pleroma.Web.OStatus do
alias Pleroma.Web import Ecto.Query
require Logger
alias Pleroma.{Repo, User, Web}
alias Pleroma.Web.ActivityPub.ActivityPub
def feed_path(user) do def feed_path(user) do
"#{user.ap_id}/feed.atom" "#{user.ap_id}/feed.atom"
@ -9,6 +13,132 @@ def pubsub_path(user) do
"#{Web.base_url}/push/hub/#{user.nickname}" "#{Web.base_url}/push/hub/#{user.nickname}"
end end
def user_path(user) do def salmon_path(user) do
"#{user.ap_id}/salmon"
end
def handle_incoming(xml_string) do
{doc, _rest} = :xmerl_scan.string(to_charlist(xml_string))
{:xmlObj, :string, object_type } = :xmerl_xpath.string('string(/entry/activity:object-type[1])', doc)
case object_type do
'http://activitystrea.ms/schema/1.0/note' ->
handle_note(doc)
_ ->
Logger.error("Couldn't parse incoming document")
end
end
# TODO
# Parse mention
# wire up replies
# Set correct context
# Set correct statusnet ids.
def handle_note(doc) do
content_html = string_from_xpath("/entry/content[1]", doc)
[author] = :xmerl_xpath.string('/entry/author[1]', doc)
{:ok, actor} = find_or_make_user(author)
context = ActivityPub.generate_context_id
to = [
"https://www.w3.org/ns/activitystreams#Public"
]
date = string_from_xpath("/entry/published", doc)
object = %{
"type" => "Note",
"to" => to,
"content" => content_html,
"published" => date,
"context" => context,
"actor" => actor.ap_id
}
ActivityPub.create(to, actor, context, object, %{}, date)
end
def find_or_make(author, doc) do
query = from user in User,
where: user.local == false and fragment("? @> ?", user.info, ^%{ostatus_uri: author})
user = Repo.one(query)
if is_nil(user) do
make_user(doc)
else
{:ok, user}
end
end
def find_or_make_user(author_doc) do
{:xmlObj, :string, uri } = :xmerl_xpath.string('string(/author[1]/uri)', author_doc)
query = from user in User,
where: user.local == false and fragment("? @> ?", user.info, ^%{ostatus_uri: to_string(uri)})
user = Repo.one(query)
if is_nil(user) do
make_user(author_doc)
else
{:ok, user}
end
end
defp string_from_xpath(xpath, doc) do
{:xmlObj, :string, res} = :xmerl_xpath.string('string(#{xpath})', doc)
res = res
|> to_string
|> String.trim
if res == "", do: nil, else: res
end
def make_user(author_doc) do
author = string_from_xpath("/author[1]/uri", author_doc)
name = string_from_xpath("/author[1]/name", author_doc)
preferredUsername = string_from_xpath("/author[1]/poco:preferredUsername", author_doc)
displayName = string_from_xpath("/author[1]/poco:displayName", author_doc)
avatar = make_avatar_object(author_doc)
data = %{
local: false,
name: preferredUsername || name,
nickname: displayName || name,
ap_id: author,
info: %{
"ostatus_uri" => author,
"host" => URI.parse(author).host,
"system" => "ostatus"
},
avatar: avatar
}
Repo.insert(Ecto.Changeset.change(%User{}, data))
end
# TODO: Just takes the first one for now.
defp make_avatar_object(author_doc) do
href = string_from_xpath("/author[1]/link[@rel=\"avatar\"]/@href", author_doc)
type = string_from_xpath("/author[1]/link[@rel=\"avatar\"]/@type", author_doc)
if href do
%{
"type" => "Image",
"url" =>
[%{
"type" => "Link",
"mediaType" => type,
"href" => href
}]
}
else
nil
end
end end
end end

View file

@ -25,7 +25,14 @@ def feed(conn, %{"nickname" => nickname}) do
|> send_resp(200, response) |> send_resp(200, response)
end end
def temp(conn, params) do def salmon_incoming(conn, params) do
IO.inspect(params) {:ok, body, _conn} = read_body(conn)
magic_key = Pleroma.Web.Salmon.fetch_magic_key(body)
{:ok, doc} = Pleroma.Web.Salmon.decode_and_validate(magic_key, body)
Pleroma.Web.OStatus.handle_incoming(doc)
conn
|> send_resp(200, "")
end end
end end

View file

@ -74,6 +74,7 @@ def user_fetcher(username) do
pipe_through :ostatus pipe_through :ostatus
get "/users/:nickname/feed", OStatus.OStatusController, :feed get "/users/:nickname/feed", OStatus.OStatusController, :feed
post "/users/:nickname/salmon", OStatus.OStatusController, :salmon_incoming
post "/push/hub/:nickname", Websub.WebsubController, :websub_subscription_request post "/push/hub/:nickname", Websub.WebsubController, :websub_subscription_request
end end

View file

@ -28,11 +28,33 @@ def create_status(user = %User{}, data = %{}) do
date = make_date() date = make_date()
activity = %{ # Wire up reply info.
"type" => "Create", [to, context, object, additional] =
"to" => to, with inReplyToId when not is_nil(inReplyToId) <- data["in_reply_to_status_id"],
"actor" => user.ap_id, inReplyTo <- Repo.get(Activity, inReplyToId),
"object" => %{ context <- inReplyTo.data["context"]
do
to = to ++ [inReplyTo.data["actor"]]
object = %{
"type" => "Note",
"to" => to,
"content" => content_html,
"published" => date,
"context" => context,
"attachment" => attachments,
"actor" => user.ap_id,
"inReplyTo" => inReplyTo.data["object"]["id"],
"inReplyToStatusId" => inReplyToId,
"statusnetConversationId" => inReplyTo.data["statusnetConversationId"]
}
additional = %{
"statusnetConversationId" => inReplyTo.data["statusnetConversationId"]
}
[to, context, object, additional]
else _e ->
object = %{
"type" => "Note", "type" => "Note",
"to" => to, "to" => to,
"content" => content_html, "content" => content_html,
@ -40,36 +62,11 @@ def create_status(user = %User{}, data = %{}) do
"context" => context, "context" => context,
"attachment" => attachments, "attachment" => attachments,
"actor" => user.ap_id "actor" => user.ap_id
}, }
"published" => date, [to, context, object, %{}]
"context" => context
}
# Wire up reply info.
activity = with inReplyToId when not is_nil(inReplyToId) <- data["in_reply_to_status_id"],
inReplyTo <- Repo.get(Activity, inReplyToId),
context <- inReplyTo.data["context"]
do
to = activity["to"] ++ [inReplyTo.data["actor"]]
activity
|> put_in(["to"], to)
|> put_in(["context"], context)
|> put_in(["object", "context"], context)
|> put_in(["object", "inReplyTo"], inReplyTo.data["object"]["id"])
|> put_in(["object", "inReplyToStatusId"], inReplyToId)
|> put_in(["statusnetConversationId"], inReplyTo.data["statusnetConversationId"])
|> put_in(["object", "statusnetConversationId"], inReplyTo.data["statusnetConversationId"])
else _e ->
activity
end
with {:ok, activity} <- ActivityPub.insert(activity) do
{:ok, activity} = add_conversation_id(activity)
Pleroma.Web.Websub.publish(Pleroma.Web.OStatus.feed_path(user), user, activity)
{:ok, activity}
end end
ActivityPub.create(to, user, context, object, additional, data)
end end
def fetch_friend_statuses(user, opts \\ %{}) do def fetch_friend_statuses(user, opts \\ %{}) do

View file

@ -31,7 +31,8 @@ def represent_user(user) do
[ [
{:Subject, "acct:#{user.nickname}@#{Pleroma.Web.host}"}, {:Subject, "acct:#{user.nickname}@#{Pleroma.Web.host}"},
{:Alias, user.ap_id}, {:Alias, user.ap_id},
{:Link, %{rel: "http://schemas.google.com/g/2010#updates-from", type: "application/atom+xml", href: OStatus.feed_path(user)}} {:Link, %{rel: "http://schemas.google.com/g/2010#updates-from", type: "application/atom+xml", href: OStatus.feed_path(user)}},
{:Link, %{rel: "salmon", href: OStatus.salmon_path(user)}}
] ]
} }
|> XmlBuilder.to_doc |> XmlBuilder.to_doc

View file

@ -27,6 +27,7 @@ test "returns a feed of the last 20 items of the user" do
<title>#{user.nickname}'s timeline</title> <title>#{user.nickname}'s timeline</title>
<updated>#{most_recent_update}</updated> <updated>#{most_recent_update}</updated>
<link rel="hub" href="#{OStatus.pubsub_path(user)}" /> <link rel="hub" href="#{OStatus.pubsub_path(user)}" />
<link rel="salmon" href="#{OStatus.salmon_path(user)}" />
<link rel="self" href="#{OStatus.feed_path(user)}" /> <link rel="self" href="#{OStatus.feed_path(user)}" />
<author> <author>
#{user_xml} #{user_xml}

View file

@ -0,0 +1,53 @@
defmodule Pleroma.Web.OStatusTest do
use Pleroma.DataCase
alias Pleroma.Web.OStatus
test "handle incoming notes" do
incoming = File.read!("test/fixtures/incoming_note_activity.xml")
{:ok, activity} = OStatus.handle_incoming(incoming)
assert activity.data["type"] == "Create"
assert activity.data["object"]["type"] == "Note"
assert activity.data["published"] == "2017-04-23T14:51:03+00:00"
end
describe "new remote user creation" do
test "make new user or find them based on an 'author' xml doc" do
incoming = File.read!("test/fixtures/user_name_only.xml")
{doc, _rest} = :xmerl_scan.string(to_charlist(incoming))
{:ok, user} = OStatus.find_or_make_user(doc)
assert user.name == "lambda"
assert user.nickname == "lambda"
assert user.local == false
assert user.info["ostatus_uri"] == "http://gs.example.org:4040/index.php/user/1"
assert user.info["system"] == "ostatus"
assert user.ap_id == "http://gs.example.org:4040/index.php/user/1"
{:ok, user_again} = OStatus.find_or_make_user(doc)
assert user == user_again
end
test "tries to use the information in poco fields" do
incoming = File.read!("test/fixtures/user_full.xml")
{doc, _rest} = :xmerl_scan.string(to_charlist(incoming))
{:ok, user} = OStatus.find_or_make_user(doc)
assert user.name == "Constance Variable"
assert user.nickname == "lambadalambda"
assert user.local == false
assert user.info["ostatus_uri"] == "http://gs.example.org:4040/index.php/user/1"
assert user.info["system"] == "ostatus"
assert user.ap_id == "http://gs.example.org:4040/index.php/user/1"
assert List.first(user.avatar["url"])["href"] == "http://gs.example.org:4040/theme/neo-gnu/default-avatar-profile.png"
{:ok, user_again} = OStatus.find_or_make_user(doc)
assert user == user_again
end
end
end