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.
{: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
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"
"#{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
|> 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
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
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,
"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
[
{: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
<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}
--- /dev/null
+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