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})
|
||||
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
|
||||
cond do
|
||||
# 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']},
|
||||
{:updated, h.(most_recent_update)},
|
||||
{: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))], []},
|
||||
{:author, UserRepresenter.to_simple_form(user)},
|
||||
] ++ entries
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
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
|
||||
"#{user.ap_id}/feed.atom"
|
||||
|
@ -9,6 +13,132 @@ def pubsub_path(user) do
|
|||
"#{Web.base_url}/push/hub/#{user.nickname}"
|
||||
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
|
||||
|
|
|
@ -25,7 +25,14 @@ def feed(conn, %{"nickname" => nickname}) do
|
|||
|> send_resp(200, response)
|
||||
end
|
||||
|
||||
def temp(conn, params) do
|
||||
IO.inspect(params)
|
||||
def salmon_incoming(conn, params) do
|
||||
{: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
|
||||
|
|
|
@ -74,6 +74,7 @@ def user_fetcher(username) do
|
|||
pipe_through :ostatus
|
||||
|
||||
get "/users/:nickname/feed", OStatus.OStatusController, :feed
|
||||
post "/users/:nickname/salmon", OStatus.OStatusController, :salmon_incoming
|
||||
post "/push/hub/:nickname", Websub.WebsubController, :websub_subscription_request
|
||||
end
|
||||
|
||||
|
|
|
@ -28,11 +28,33 @@ def create_status(user = %User{}, data = %{}) do
|
|||
|
||||
date = make_date()
|
||||
|
||||
activity = %{
|
||||
"type" => "Create",
|
||||
"to" => to,
|
||||
"actor" => user.ap_id,
|
||||
"object" => %{
|
||||
# Wire up reply info.
|
||||
[to, context, object, additional] =
|
||||
with inReplyToId when not is_nil(inReplyToId) <- data["in_reply_to_status_id"],
|
||||
inReplyTo <- Repo.get(Activity, inReplyToId),
|
||||
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",
|
||||
"to" => to,
|
||||
"content" => content_html,
|
||||
|
@ -40,36 +62,11 @@ def create_status(user = %User{}, data = %{}) do
|
|||
"context" => context,
|
||||
"attachment" => attachments,
|
||||
"actor" => user.ap_id
|
||||
},
|
||||
"published" => date,
|
||||
"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}
|
||||
}
|
||||
[to, context, object, %{}]
|
||||
end
|
||||
|
||||
ActivityPub.create(to, user, context, object, additional, data)
|
||||
end
|
||||
|
||||
def fetch_friend_statuses(user, opts \\ %{}) do
|
||||
|
|
|
@ -31,7 +31,8 @@ def represent_user(user) do
|
|||
[
|
||||
{: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)}}
|
||||
{: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
|
||||
|
|
|
@ -27,6 +27,7 @@ test "returns a feed of the last 20 items of the user" do
|
|||
<title>#{user.nickname}'s timeline</title>
|
||||
<updated>#{most_recent_update}</updated>
|
||||
<link rel="hub" href="#{OStatus.pubsub_path(user)}" />
|
||||
<link rel="salmon" href="#{OStatus.salmon_path(user)}" />
|
||||
<link rel="self" href="#{OStatus.feed_path(user)}" />
|
||||
<author>
|
||||
#{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