forked from AkkomaGang/akkoma
Return salmon path for users, basic incoming salmon handling.
This commit is contained in:
parent
34d3aea92f
commit
ab0114fbaa
9 changed files with 272 additions and 39 deletions
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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}
|
||||||
|
|
53
test/web/ostatus/ostatus_test.exs
Normal file
53
test/web/ostatus/ostatus_test.exs
Normal 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
|
Loading…
Reference in a new issue