defmodule Pleroma.User do
use Ecto.Schema
- import Ecto.Changeset
- import Ecto.Query
- alias Pleroma.{Repo, User, Object}
+
+ import Ecto.{Changeset, Query}
+ alias Pleroma.{Repo, User, Object, Web}
+ alias Comeonin.Pbkdf2
+ alias Pleroma.Web.{OStatus, Websub}
+ alias Pleroma.Web.ActivityPub.ActivityPub
schema "users" do
field :bio, :string
field :password_hash, :string
field :password, :string, virtual: true
field :password_confirmation, :string, virtual: true
- field :following, { :array, :string }, default: []
+ field :following, {:array, :string}, default: []
field :ap_id, :string
field :avatar, :map
+ field :local, :boolean, default: true
+ field :info, :map, default: %{}
timestamps()
end
end
def ap_id(%User{nickname: nickname}) do
- "#{Pleroma.Web.base_url}/users/#{nickname}"
+ "#{Web.base_url}/users/#{nickname}"
end
def ap_followers(%User{} = user) do
|> validate_format(:nickname, ~r/^[a-zA-Z\d]+$/)
if changeset.valid? do
- hashed = Comeonin.Pbkdf2.hashpwsalt(changeset.changes[:password])
+ hashed = Pbkdf2.hashpwsalt(changeset.changes[:password])
ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]})
followers = User.ap_followers(%User{nickname: changeset.changes[:nickname]})
changeset
def follow(%User{} = follower, %User{} = followed) do
ap_followers = User.ap_followers(followed)
if following?(follower, followed) do
- { :error,
- "Could not follow user: #{followed.nickname} is already on your list." }
+ {:error,
+ "Could not follow user: #{followed.nickname} is already on your list."}
else
+ if !followed.local do
+ Websub.subscribe(follower, followed)
+ end
+
following = [ap_followers | follower.following]
|> Enum.uniq
following = follower.following
|> List.delete(ap_followers)
- follower
+ { :ok, follower } = follower
|> follow_changeset(%{following: following})
|> Repo.update
+ { :ok, follower, ActivityPub.fetch_latest_follow(follower, followed)}
else
- { :error, "Not subscribed!" }
+ {:error, "Not subscribed!"}
end
end
def get_cached_by_nickname(nickname) do
key = "nickname:#{nickname}"
- Cachex.get!(:user_cache, key, fallback: fn(_) -> Repo.get_by(User, nickname: nickname) end)
+ Cachex.get!(:user_cache, key, fallback: fn(_) -> get_or_fetch_by_nickname(nickname) end)
+ end
+
+ def get_by_nickname(nickname) do
+ Repo.get_by(User, nickname: nickname)
+ end
+
+ def get_cached_user_info(user) do
+ key = "user_info:#{user.id}"
+ Cachex.get!(:user_cache, key, fallback: fn(_) -> user_info(user) end)
+ end
+
+ def get_or_fetch_by_nickname(nickname) do
+ with %User{} = user <- get_by_nickname(nickname) do
+ user
+ else _e ->
+ with [nick, domain] <- String.split(nickname, "@"),
+ {:ok, user} <- OStatus.make_user(nickname) do
+ user
+ else _e -> nil
+ end
+ end
end
end
defmodule Pleroma.Web.ActivityPub.ActivityPub do
- alias Pleroma.Repo
- alias Pleroma.{Activity, Object, Upload, User}
+ alias Pleroma.{Activity, Repo, Object, Upload, User, Web}
+ alias Ecto.{Changeset, UUID}
import Ecto.Query
- def insert(map) when is_map(map) do
+ def insert(map, local \\ true) when is_map(map) do
map = map
|> Map.put_new_lazy("id", &generate_activity_id/0)
|> Map.put_new_lazy("published", &make_date/0)
map
end
- Repo.insert(%Activity{data: map})
+ Repo.insert(%Activity{data: map, local: local})
end
- def like(%User{ap_id: ap_id} = user, %Object{data: %{ "id" => id}} = object) do
+ def create(to, actor, context, object, additional \\ %{}, published \\ nil, local \\ true) do
+ published = published || make_date()
+
+ activity = %{
+ "type" => "Create",
+ "to" => to |> Enum.uniq,
+ "actor" => actor.ap_id,
+ "object" => object,
+ "published" => published,
+ "context" => context
+ }
+ |> Map.merge(additional)
+
+ with {:ok, activity} <- insert(activity, local) do
+ if actor.local do
+ Pleroma.Web.Federator.enqueue(:publish, activity)
+ end
+
+ {:ok, activity}
+ end
+ end
+
+ def like(%User{ap_id: ap_id} = user, %Object{data: %{"id" => id}} = object, local \\ true) do
cond do
# There's already a like here, so return the original activity.
ap_id in (object.data["likes"] || []) ->
"type" => "Like",
"actor" => ap_id,
"object" => id,
- "to" => [User.ap_followers(user), object.data["actor"]]
+ "to" => [User.ap_followers(user), object.data["actor"]],
+ "context" => object.data["context"]
}
- {:ok, activity} = insert(data)
+ {:ok, activity} = insert(data, local)
likes = [ap_id | (object.data["likes"] || [])] |> Enum.uniq
|> Map.put("like_count", length(likes))
|> Map.put("likes", likes)
- changeset = Ecto.Changeset.change(object, data: new_data)
+ changeset = Changeset.change(object, data: new_data)
{:ok, object} = Repo.update(changeset)
update_object_in_activities(object)
+ if user.local do
+ Pleroma.Web.Federator.enqueue(:publish, activity)
+ end
+
{:ok, activity, object}
end
end
relevant_activities = Activity.all_by_object_ap_id(id)
Enum.map(relevant_activities, fn (activity) ->
new_activity_data = activity.data |> Map.put("object", object.data)
- changeset = Ecto.Changeset.change(activity, data: new_activity_data)
+ changeset = Changeset.change(activity, data: new_activity_data)
Repo.update(changeset)
end)
end
|> Map.put("like_count", length(likes))
|> Map.put("likes", likes)
- changeset = Ecto.Changeset.change(object, data: new_data)
+ changeset = Changeset.change(object, data: new_data)
{:ok, object} = Repo.update(changeset)
update_object_in_activities(object)
end
def generate_object_id do
- generate_id("objects")
+ Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :object, Ecto.UUID.generate)
end
def generate_id(type) do
- "#{Pleroma.Web.base_url()}/#{type}/#{Ecto.UUID.generate}"
+ "#{Web.base_url()}/#{type}/#{UUID.generate}"
end
def fetch_public_activities(opts \\ %{}) do
query = from activity in query,
where: activity.id > ^since_id
+ query = if opts["local_only"] do
+ from activity in query, where: activity.local == true
+ else
+ query
+ end
+
query = if opts["max_id"] do
from activity in query, where: activity.id < ^opts["max_id"]
else
query
end
- Repo.all(query)
- |> Enum.reverse
+ Enum.reverse(Repo.all(query))
end
- def announce(%User{ap_id: ap_id} = user, %Object{data: %{"id" => id}} = object) do
+ def announce(%User{ap_id: ap_id} = user, %Object{data: %{"id" => id}} = object, local \\ true) do
data = %{
"type" => "Announce",
"actor" => ap_id,
"object" => id,
- "to" => [User.ap_followers(user), object.data["actor"]]
+ "to" => [User.ap_followers(user), object.data["actor"]],
+ "context" => object.data["context"]
}
- {:ok, activity} = insert(data)
+ {:ok, activity} = insert(data, local)
announcements = [ap_id | (object.data["announcements"] || [])] |> Enum.uniq
|> Map.put("announcement_count", length(announcements))
|> Map.put("announcements", announcements)
- changeset = Ecto.Changeset.change(object, data: new_data)
+ changeset = Changeset.change(object, data: new_data)
{:ok, object} = Repo.update(changeset)
update_object_in_activities(object)
+ if user.local do
+ Pleroma.Web.Federator.enqueue(:publish, activity)
+ end
+
{:ok, activity, object}
end
++ def follow(%User{ap_id: follower_id, local: actor_local}, %User{ap_id: followed_id}, local \\ true) do
++ data = %{
++ "type" => "Follow",
++ "actor" => follower_id,
++ "to" => [followed_id],
++ "object" => followed_id,
++ "published" => make_date()
++ }
++
++ with {:ok, activity} <- insert(data, local) do
++ if actor_local do
++ Pleroma.Web.Federator.enqueue(:publish, activity)
++ end
++
++ {:ok, activity}
++ end
++ end
++
++ def unfollow(follower, followed, local \\ true) do
++ with follow_activity when not is_nil(follow_activity) <- fetch_latest_follow(follower, followed) do
++ data = %{
++ "type" => "Undo",
++ "actor" => follower.ap_id,
++ "to" => [followed.ap_id],
++ "object" => follow_activity.data["id"],
++ "published" => make_date()
++ }
++
++ with {:ok, activity} <- insert(data, local) do
++ if follower.local do
++ Pleroma.Web.Federator.enqueue(:publish, activity)
++ end
++
++ {:ok, activity}
++ end
++ end
++ end
++
def fetch_activities_for_context(context) do
query = from activity in Activity,
where: fragment("? @> ?", activity.data, ^%{ context: context })
Repo.all(query)
end
+ def fetch_latest_follow(%User{ap_id: follower_id},
+ %User{ap_id: followed_id}) do
+ query = from activity in Activity,
+ where: fragment("? @> ?", activity.data, ^%{type: "Follow", actor: follower_id,
+ object: followed_id}),
+ order_by: [desc: :inserted_at],
+ limit: 1
+ Repo.one(query)
+ end
+
def upload(file) do
data = Upload.store(file)
Repo.insert(%Object{data: data})
defmodule Pleroma.Web.OStatus.ActivityRepresenter do
- def to_simple_form(%{data: %{"object" => %{"type" => "Note"}}} = activity, user) do
+ alias Pleroma.{Activity, User}
+ alias Pleroma.Web.OStatus.UserRepresenter
+ require Logger
+
+ defp get_in_reply_to(%{"object" => %{"inReplyTo" => in_reply_to}}) do
+ [{:"thr:in-reply-to", [ref: to_charlist(in_reply_to)], []}]
+ end
+
+ defp get_in_reply_to(_), do: []
+
+ defp get_mentions(to) do
+ Enum.map(to, fn (id) ->
+ cond do
+ # Special handling for the AP/Ostatus public collections
+ "https://www.w3.org/ns/activitystreams#Public" == id ->
+ {:link, [rel: "mentioned", "ostatus:object-type": "http://activitystrea.ms/schema/1.0/collection", href: "http://activityschema.org/collection/public"], []}
+ # Ostatus doesn't handle follower collections, ignore these.
+ Regex.match?(~r/^#{Pleroma.Web.base_url}.+followers$/, id) ->
+ []
+ true ->
+ {:link, [rel: "mentioned", "ostatus:object-type": "http://activitystrea.ms/schema/1.0/person", href: id], []}
+ end
+ end)
+ end
+
+ def to_simple_form(activity, user, with_author \\ false)
+ def to_simple_form(%{data: %{"object" => %{"type" => "Note"}}} = activity, user, with_author) do
h = fn(str) -> [to_charlist(str)] end
updated_at = activity.updated_at
{:link, [rel: 'enclosure', href: to_charlist(url["href"]), type: to_charlist(url["mediaType"])], []}
end)
+ in_reply_to = get_in_reply_to(activity.data)
+ author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: []
+ mentions = activity.data["to"] |> get_mentions
+
[
{:"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"])},
+ {:id, h.(activity.data["object"]["id"])}, # For notes, federate the 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
+ {:updated, h.(updated_at)},
+ {:"ostatus:conversation", [], h.(activity.data["context"])},
+ {:link, [href: h.(activity.data["context"]), rel: 'ostatus:conversation'], []},
+ {:link, [type: ['application/atom+xml'], href: h.(activity.data["object"]["id"]), rel: 'self'], []}
+ ] ++ attachments ++ in_reply_to ++ author ++ mentions
+ end
+
+ def to_simple_form(%{data: %{"type" => "Like"}} = activity, user, with_author) do
+ h = fn(str) -> [to_charlist(str)] end
+
+ updated_at = activity.updated_at
+ |> NaiveDateTime.to_iso8601
+ inserted_at = activity.inserted_at
+ |> NaiveDateTime.to_iso8601
+
+ in_reply_to = get_in_reply_to(activity.data)
+ author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: []
+ mentions = activity.data["to"] |> get_mentions
+
+ [
+ {:"activity:verb", ['http://activitystrea.ms/schema/1.0/favorite']},
+ {:id, h.(activity.data["id"])},
+ {:title, ['New favorite by #{user.nickname}']},
+ {:content, [type: 'html'], ['#{user.nickname} favorited something']},
+ {:published, h.(inserted_at)},
+ {:updated, h.(updated_at)},
+ {:"activity:object", [
+ {:"activity:object-type", ['http://activitystrea.ms/schema/1.0/note']},
+ {:id, h.(activity.data["object"])}, # For notes, federate the object id.
+ ]},
+ {:"ostatus:conversation", [], h.(activity.data["context"])},
+ {:link, [href: h.(activity.data["context"]), rel: 'ostatus:conversation'], []},
+ {:link, [rel: 'self', type: ['application/atom+xml'], href: h.(activity.data["id"])], []},
+ {:"thr:in-reply-to", [ref: to_charlist(activity.data["object"])], []}
+ ] ++ author ++ mentions
+ end
+
+ def to_simple_form(%{data: %{"type" => "Announce"}} = activity, user, with_author) do
+ h = fn(str) -> [to_charlist(str)] end
+
+ updated_at = activity.updated_at
+ |> NaiveDateTime.to_iso8601
+ inserted_at = activity.inserted_at
+ |> NaiveDateTime.to_iso8601
+
+ in_reply_to = get_in_reply_to(activity.data)
+ author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: []
+
+ retweeted_activity = Activity.get_create_activity_by_object_ap_id(activity.data["object"])
+ retweeted_user = User.get_cached_by_ap_id(retweeted_activity.data["actor"])
+
+ retweeted_xml = to_simple_form(retweeted_activity, retweeted_user, true)
+
+ mentions = activity.data["to"] |> get_mentions
+ [
+ {:"activity:object-type", ['http://activitystrea.ms/schema/1.0/activity']},
+ {:"activity:verb", ['http://activitystrea.ms/schema/1.0/share']},
+ {:id, h.(activity.data["id"])},
+ {:title, ['#{user.nickname} repeated a notice']},
+ {:content, [type: 'html'], ['RT #{retweeted_activity.data["object"]["content"]}']},
+ {:published, h.(inserted_at)},
+ {:updated, h.(updated_at)},
+ {:"ostatus:conversation", [], h.(activity.data["context"])},
+ {:link, [href: h.(activity.data["context"]), rel: 'ostatus:conversation'], []},
+ {:link, [rel: 'self', type: ['application/atom+xml'], href: h.(activity.data["id"])], []},
+ {:"activity:object", retweeted_xml}
+ ] ++ mentions ++ author
+ end
+
+ def to_simple_form(%{data: %{"type" => "Follow"}} = activity, user, with_author) do
+ h = fn(str) -> [to_charlist(str)] end
+
+ updated_at = activity.updated_at
+ |> NaiveDateTime.to_iso8601
+ inserted_at = activity.inserted_at
+ |> NaiveDateTime.to_iso8601
+
+ author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: []
+
+ mentions = (activity.data["to"] || []) |> get_mentions
+ [
+ {:"activity:object-type", ['http://activitystrea.ms/schema/1.0/activity']},
+ {:"activity:verb", ['http://activitystrea.ms/schema/1.0/follow']},
+ {:id, h.(activity.data["id"])},
+ {:title, ['#{user.nickname} started following #{activity.data["object"]}']},
+ {:content, [type: 'html'], ['#{user.nickname} started following #{activity.data["object"]}']},
+ {:published, h.(inserted_at)},
+ {:updated, h.(updated_at)},
+ {:"activity:object", [
+ {:"activity:object-type", ['http://activitystrea.ms/schema/1.0/person']},
+ {:id, h.(activity.data["object"])},
+ {:uri, h.(activity.data["object"])},
+ ]},
+ {:link, [rel: 'self', type: ['application/atom+xml'], href: h.(activity.data["id"])], []},
+ ] ++ mentions ++ author
+ end
+
++ # Only undos of follow for now. Will need to get redone once there are more
++ def to_simple_form(%{data: %{"type" => "Undo"}} = activity, user, with_author) do
++ h = fn(str) -> [to_charlist(str)] end
++
++ updated_at = activity.updated_at
++ |> NaiveDateTime.to_iso8601
++ inserted_at = activity.inserted_at
++ |> NaiveDateTime.to_iso8601
++
++ author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: []
++ follow_activity = Activity.get_by_ap_id(activity.data["object"])
++
++ mentions = (activity.data["to"] || []) |> get_mentions
++ [
++ {:"activity:object-type", ['http://activitystrea.ms/schema/1.0/activity']},
++ {:"activity:verb", ['http://activitystrea.ms/schema/1.0/unfollow']},
++ {:id, h.(activity.data["id"])},
++ {:title, ['#{user.nickname} stopped following #{follow_activity.data["object"]}']},
++ {:content, [type: 'html'], ['#{user.nickname} stopped following #{follow_activity.data["object"]}']},
++ {:published, h.(inserted_at)},
++ {:updated, h.(updated_at)},
++ {:"activity:object", [
++ {:"activity:object-type", ['http://activitystrea.ms/schema/1.0/person']},
++ {:id, h.(follow_activity.data["object"])},
++ {:uri, h.(follow_activity.data["object"])},
++ ]},
++ {:link, [rel: 'self', type: ['application/atom+xml'], href: h.(activity.data["id"])], []},
++ ] ++ mentions ++ author
++ end
++
+ def wrap_with_entry(simple_form) do
+ [{
+ :entry, [
+ xmlns: 'http://www.w3.org/2005/Atom',
+ "xmlns:thr": 'http://purl.org/syndication/thread/1.0',
+ "xmlns:activity": 'http://activitystrea.ms/spec/1.0/',
+ "xmlns:poco": 'http://portablecontacts.net/spec/1.0',
+ "xmlns:ostatus": 'http://ostatus.org/schema/1.0'
+ ], simple_form
+ }]
end
- def to_simple_form(_,_), do: nil
+ def to_simple_form(_, _, _), do: nil
end
defmodule Pleroma.Web.OStatus do
- alias Pleroma.Web
+ @httpoison Application.get_env(:pleroma, :httpoison)
+
+ import Ecto.Query
+ import Pleroma.Web.XML
+ require Logger
+
+ alias Pleroma.{Repo, User, Web, Object, Activity}
+ alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.{WebFinger, Websub}
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 = parse_document(xml_string)
+ entries = :xmerl_xpath.string('//entry', doc)
+
+ activities = Enum.map(entries, fn (entry) ->
+ {:xmlObj, :string, object_type} = :xmerl_xpath.string('string(/entry/activity:object-type[1])', entry)
+ {:xmlObj, :string, verb} = :xmerl_xpath.string('string(/entry/activity:verb[1])', entry)
+
+ case verb do
+ 'http://activitystrea.ms/schema/1.0/share' ->
+ with {:ok, activity, retweeted_activity} <- handle_share(entry, doc), do: [activity, retweeted_activity]
+ 'http://activitystrea.ms/schema/1.0/favorite' ->
+ with {:ok, activity, favorited_activity} <- handle_favorite(entry, doc), do: [activity, favorited_activity]
+ _ ->
+ case object_type do
+ 'http://activitystrea.ms/schema/1.0/note' ->
+ with {:ok, activity} <- handle_note(entry, doc), do: activity
+ 'http://activitystrea.ms/schema/1.0/comment' ->
+ with {:ok, activity} <- handle_note(entry, doc), do: activity
+ _ ->
+ Logger.error("Couldn't parse incoming document")
+ nil
+ end
+ end
+ end)
+ {:ok, activities}
+ end
+
+ def make_share(_entry, doc, retweeted_activity) do
+ with {:ok, actor} <- find_make_or_update_user(doc),
+ %Object{} = object <- Object.get_cached_by_ap_id(retweeted_activity.data["object"]["id"]),
+ {:ok, activity, _object} = ActivityPub.announce(actor, object, false) do
+ {:ok, activity}
+ end
+ end
+
+ def handle_share(entry, doc) do
+ with [object] <- :xmerl_xpath.string('/entry/activity:object', entry),
+ {:ok, retweeted_activity} <- handle_note(object, object),
+ {:ok, activity} <- make_share(entry, doc, retweeted_activity) do
+ {:ok, activity, retweeted_activity}
+ else
+ e -> {:error, e}
+ end
+ end
+
+ def make_favorite(_entry, doc, favorited_activity) do
+ with {:ok, actor} <- find_make_or_update_user(doc),
+ %Object{} = object <- Object.get_cached_by_ap_id(favorited_activity.data["object"]["id"]),
+ {:ok, activity, _object} = ActivityPub.like(actor, object, false) do
+ {:ok, activity}
+ end
+ end
+
+ def get_or_try_fetching(entry) do
+ with id when not is_nil(id) <- string_from_xpath("//activity:object[1]/id", entry),
+ %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
+ {:ok, activity}
+ else _e ->
+ with href when not is_nil(href) <- string_from_xpath("//activity:object[1]/link[@type=\"text/html\"]/@href", entry),
+ {:ok, [favorited_activity]} <- fetch_activity_from_html_url(href) do
+ {:ok, favorited_activity}
+ end
+ end
+ end
+
+ def handle_favorite(entry, doc) do
+ with {:ok, favorited_activity} <- get_or_try_fetching(entry),
+ {:ok, activity} <- make_favorite(entry, doc, favorited_activity) do
+ {:ok, activity, favorited_activity}
+ else
+ e -> {:error, e}
+ end
+ end
+
+ def get_attachments(entry) do
+ :xmerl_xpath.string('/entry/link[@rel="enclosure"]', entry)
+ |> Enum.map(fn (enclosure) ->
+ with href when not is_nil(href) <- string_from_xpath("/link/@href", enclosure),
+ type when not is_nil(type) <- string_from_xpath("/link/@type", enclosure) do
+ %{
+ "type" => "Attachment",
+ "url" => [%{
+ "type" => "Link",
+ "mediaType" => type,
+ "href" => href
+ }]
+ }
+ end
+ end)
+ |> Enum.filter(&(&1))
+ end
+
+ def handle_note(entry, doc \\ nil) do
+ content_html = string_from_xpath("//content[1]", entry)
+
+ [author] = :xmerl_xpath.string('//author[1]', doc)
+ {:ok, actor} = find_make_or_update_user(author)
+ inReplyTo = string_from_xpath("//thr:in-reply-to[1]/@ref", entry)
+
+ if !Object.get_cached_by_ap_id(inReplyTo) do
+ inReplyToHref = string_from_xpath("//thr:in-reply-to[1]/@href", entry)
+ if inReplyToHref do
+ fetch_activity_from_html_url(inReplyToHref)
+ end
+ end
+
+ context = (string_from_xpath("//ostatus:conversation[1]", entry) || "") |> String.trim
+
+ attachments = get_attachments(entry)
+
+ context = with %{data: %{"context" => context}} <- Object.get_cached_by_ap_id(inReplyTo) do
+ context
+ else _e ->
+ if String.length(context) > 0 do
+ context
+ else
+ ActivityPub.generate_context_id
+ end
+ end
+
+ to = [
+ "https://www.w3.org/ns/activitystreams#Public",
+ User.ap_followers(actor)
+ ]
+
+ mentions = :xmerl_xpath.string('//link[@rel="mentioned" and @ostatus:object-type="http://activitystrea.ms/schema/1.0/person"]', entry)
+ |> Enum.map(fn(person) -> string_from_xpath("@href", person) end)
+
+ to = to ++ mentions
+
+ date = string_from_xpath("//published", entry)
+ id = string_from_xpath("//id", entry)
+
+ object = %{
+ "id" => id,
+ "type" => "Note",
+ "to" => to,
+ "content" => content_html,
+ "published" => date,
+ "context" => context,
+ "actor" => actor.ap_id,
+ "attachment" => attachments
+ }
+
+ object = if inReplyTo do
+ Map.put(object, "inReplyTo", inReplyTo)
+ else
+ object
+ end
+
+ # TODO: Bail out sooner and use transaction.
+ if Object.get_by_ap_id(id) do
+ {:error, "duplicate activity"}
+ else
+ ActivityPub.create(to, actor, context, object, %{}, date, false)
+ end
+ end
+
+ def find_make_or_update_user(doc) do
+ uri = string_from_xpath("//author/uri[1]", doc)
+ with {:ok, user} <- find_or_make_user(uri) do
+ avatar = make_avatar_object(doc)
- if user.avatar != avatar do
++ if !user.local && user.avatar != avatar do
+ change = Ecto.Changeset.change(user, %{avatar: avatar})
+ Repo.update(change)
+ else
+ {:ok, user}
+ end
+ end
+ end
+
+ def find_or_make_user(uri) do
+ query = from user in User,
+ where: user.ap_id == ^uri
+
+ user = Repo.one(query)
+
+ if is_nil(user) do
+ make_user(uri)
+ else
+ {:ok, user}
+ end
+ end
+
+ def make_user(uri) do
+ with {:ok, info} <- gather_user_info(uri) do
+ data = %{
+ local: false,
+ name: info["name"],
+ nickname: info["nickname"] <> "@" <> info["host"],
+ ap_id: info["uri"],
+ info: info,
+ avatar: info["avatar"]
+ }
+ # TODO: Make remote user changeset
+ # SHould enforce fqn nickname
+ Repo.insert(Ecto.Changeset.change(%User{}, data))
+ end
+ end
+
+ # TODO: Just takes the first one for now.
+ def 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
+
+ def gather_user_info(username) do
+ with {:ok, webfinger_data} <- WebFinger.finger(username),
+ {:ok, feed_data} <- Websub.gather_feed_data(webfinger_data["topic"]) do
+ {:ok, Map.merge(webfinger_data, feed_data) |> Map.put("fqn", username)}
+ else e ->
+ Logger.debug(fn -> "Couldn't gather info for #{username}" end)
+ {:error, e}
+ end
+ end
+
+ # Regex-based 'parsing' so we don't have to pull in a full html parser
+ # It's a hack anyway. Maybe revisit this in the future
+ @mastodon_regex ~r/<link href='(.*)' rel='alternate' type='application\/atom\+xml'>/
+ @gs_regex ~r/<link title=.* href="(.*)" type="application\/atom\+xml" rel="alternate">/
+ @gs_classic_regex ~r/<link rel="alternate" href="(.*)" type="application\/atom\+xml" title=.*>/
+ def get_atom_url(body) do
+ cond do
+ Regex.match?(@mastodon_regex, body) ->
+ [[_, match]] = Regex.scan(@mastodon_regex, body)
+ {:ok, match}
+ Regex.match?(@gs_regex, body) ->
+ [[_, match]] = Regex.scan(@gs_regex, body)
+ {:ok, match}
+ Regex.match?(@gs_classic_regex, body) ->
+ [[_, match]] = Regex.scan(@gs_classic_regex, body)
+ {:ok, match}
+ true ->
+ Logger.debug(fn -> "Couldn't find atom link in #{inspect(body)}" end)
+ {:error, "Couldn't find the atom link"}
+ end
+ end
+
+ def fetch_activity_from_html_url(url) do
+ with {:ok, %{body: body}} <- @httpoison.get(url, [], follow_redirect: true),
+ {:ok, atom_url} <- get_atom_url(body),
+ {:ok, %{status_code: code, body: body}} when code in 200..299 <- @httpoison.get(atom_url, [], follow_redirect: true) do
+ handle_incoming(body)
+ end
end
end
defmodule Pleroma.Web.TwitterAPI.Representers.ActivityRepresenter do
use Pleroma.Web.TwitterAPI.Representers.BaseRepresenter
alias Pleroma.Web.TwitterAPI.Representers.{UserRepresenter, ObjectRepresenter}
- alias Pleroma.Activity
-
+ alias Pleroma.{Activity, User}
+ alias Calendar.Strftime
+ alias Pleroma.Web.TwitterAPI.TwitterAPI
++ alias Pleroma.Wi
defp user_by_ap_id(user_list, ap_id) do
Enum.find(user_list, fn (%{ap_id: user_id}) -> ap_id == user_id end)
end
- def to_map(%Activity{data: %{"type" => "Announce", "actor" => actor}} = activity, %{users: users, announced_activity: announced_activity} = opts) do
+ def to_map(%Activity{data: %{"type" => "Announce", "actor" => actor, "published" => created_at}} = activity,
+ %{users: users, announced_activity: announced_activity} = opts) do
user = user_by_ap_id(users, actor)
- created_at = get_in(activity.data, ["published"])
- |> date_to_asctime
+ created_at = created_at |> date_to_asctime
text = "#{user.nickname} retweeted a status."
"is_post_verb" => false,
"uri" => "tag:#{activity.data["id"]}:objectType=note",
"created_at" => created_at,
- "retweeted_status" => retweeted_status
+ "retweeted_status" => retweeted_status,
+ "statusnet_conversation_id" => conversation_id(announced_activity)
}
end
- def to_map(%Activity{data: %{"type" => "Like"}} = activity, %{user: user, liked_activity: liked_activity} = opts) do
- created_at = get_in(activity.data, ["published"])
- |> date_to_asctime
+ def to_map(%Activity{data: %{"type" => "Like", "published" => created_at}} = activity,
+ %{user: user, liked_activity: liked_activity} = opts) do
+ created_at = created_at |> date_to_asctime
text = "#{user.nickname} favorited a status."
%{
"id" => activity.id,
"user" => UserRepresenter.to_map(user, opts),
- "statusnet_html" => text, # TODO: add summary
+ "statusnet_html" => text,
"text" => text,
"is_local" => true,
"is_post_verb" => false,
}
end
- def to_map(%Activity{data: %{"type" => "Follow"}} = activity, %{user: user} = opts) do
- created_at = get_in(activity.data, ["published"])
- |> date_to_asctime
+ def to_map(%Activity{data: %{"type" => "Follow", "published" => created_at, "object" => followed_id}} = activity, %{user: user} = opts) do
+ created_at = created_at |> date_to_asctime
+ followed = User.get_cached_by_ap_id(followed_id)
+ text = "#{user.nickname} started following #{followed.nickname}"
%{
"id" => activity.id,
"user" => UserRepresenter.to_map(user, opts),
"attentions" => [],
- "statusnet_html" => "", # TODO: add summary
- "text" => "",
+ "statusnet_html" => text,
+ "text" => text,
"is_local" => true,
"is_post_verb" => false,
"created_at" => created_at,
}
end
- def to_map(%Activity{} = activity, %{user: user} = opts) do
- content = get_in(activity.data, ["object", "content"])
- created_at = get_in(activity.data, ["object", "published"])
- |> date_to_asctime
- like_count = get_in(activity.data, ["object", "like_count"]) || 0
- announcement_count = get_in(activity.data, ["object", "announcement_count"]) || 0
- favorited = opts[:for] && opts[:for].ap_id in (activity.data["object"]["likes"] || [])
- repeated = opts[:for] && opts[:for].ap_id in (activity.data["object"]["announcements"] || [])
+ def to_map(%Activity{data: %{"object" => %{"content" => content} = object}} = activity, %{user: user} = opts) do
+ created_at = object["published"] |> date_to_asctime
+ like_count = object["like_count"] || 0
+ announcement_count = object["announcement_count"] || 0
+ favorited = opts[:for] && opts[:for].ap_id in (object["likes"] || [])
+ repeated = opts[:for] && opts[:for].ap_id in (object["announcements"] || [])
mentions = opts[:mentioned] || []
|> Enum.filter(&(&1))
|> Enum.map(fn (user) -> UserRepresenter.to_map(user, opts) end)
+ conversation_id = conversation_id(activity)
+
%{
"id" => activity.id,
"user" => UserRepresenter.to_map(user, opts),
"is_local" => true,
"is_post_verb" => true,
"created_at" => created_at,
- "in_reply_to_status_id" => activity.data["object"]["inReplyToStatusId"],
- "statusnet_conversation_id" => activity.data["object"]["statusnetConversationId"],
- "attachments" => (activity.data["object"]["attachment"] || []) |> ObjectRepresenter.enum_to_list(opts),
+ "in_reply_to_status_id" => object["inReplyToStatusId"],
+ "statusnet_conversation_id" => conversation_id,
+ "attachments" => (object["attachment"] || []) |> ObjectRepresenter.enum_to_list(opts),
"attentions" => attentions,
"fave_num" => like_count,
"repeat_num" => announcement_count,
- "favorited" => !!favorited,
- "repeated" => !!repeated,
+ "favorited" => to_boolean(favorited),
+ "repeated" => to_boolean(repeated),
}
end
+ def conversation_id(activity) do
+ with context when not is_nil(context) <- activity.data["context"] do
+ TwitterAPI.context_to_conversation_id(context)
+ else _e -> nil
+ end
+ end
+
defp date_to_asctime(date) do
with {:ok, date, _offset} <- date |> DateTime.from_iso8601 do
- Calendar.Strftime.strftime!(date, "%a %b %d %H:%M:%S %z %Y")
+ Strftime.strftime!(date, "%a %b %d %H:%M:%S %z %Y")
else _e ->
""
end
end
+
+ defp to_boolean(false) do
+ false
+ end
+
+ defp to_boolean(nil) do
+ false
+ end
+
+ defp to_boolean(_) do
+ true
+ end
end
import Ecto.Query
- def create_status(user = %User{}, data = %{}) do
- attachments = Enum.map(data["media_ids"] || [], fn (media_id) ->
- Repo.get(Object, media_id).data
- end)
+ def to_for_user_and_mentions(user, mentions) do
+ default_to = [
+ User.ap_followers(user),
+ "https://www.w3.org/ns/activitystreams#Public"
+ ]
- context = ActivityPub.generate_context_id
+ default_to ++ Enum.map(mentions, fn ({_, %{ap_id: ap_id}}) -> ap_id end)
+ end
- content = HtmlSanitizeEx.strip_tags(data["status"])
+ def format_input(text, mentions) do
+ HtmlSanitizeEx.strip_tags(text)
|> String.replace("\n", "<br>")
+ |> add_user_links(mentions)
+ end
- mentions = parse_mentions(content)
+ def attachments_from_ids(ids) do
+ Enum.map(ids || [], fn (media_id) ->
+ Repo.get(Object, media_id).data
+ end)
+ end
- default_to = [
- User.ap_followers(user),
- "https://www.w3.org/ns/activitystreams#Public"
- ]
+ def get_replied_to_activity(id) when not is_nil(id) do
+ Repo.get(Activity, id)
+ end
- to = default_to ++ Enum.map(mentions, fn ({_, %{ap_id: ap_id}}) -> ap_id end)
+ def get_replied_to_activity(_), do: nil
- content_html = add_user_links(content, mentions)
+ def add_attachments(text, attachments) do
+ attachment_text = Enum.map(attachments, fn
+ (%{"url" => [%{"href" => href} | _]}) ->
+ "<a href='#{href}'>#{href}</a>"
+ _ -> ""
+ end)
+ Enum.join([text | attachment_text], "<br>")
+ end
+
+ def create_status(%User{} = user, %{"status" => status} = data) do
+ attachments = attachments_from_ids(data["media_ids"])
+ context = ActivityPub.generate_context_id
+ mentions = parse_mentions(status)
+ content_html = status
+ |> format_input(mentions)
+ |> add_attachments(attachments)
+ to = to_for_user_and_mentions(user, mentions)
date = make_date()
- activity = %{
- "type" => "Create",
- "to" => to,
- "actor" => user.ap_id,
- "object" => %{
+ inReplyTo = get_replied_to_activity(data["in_reply_to_status_id"])
+
+ # Wire up reply info.
+ [to, context, object, additional] =
+ if inReplyTo do
+ context = inReplyTo.data["context"]
+ 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" => inReplyTo.id,
+ }
+ additional = %{}
+
+ [to, context, object, additional]
+ else
+ 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
end
def fetch_public_statuses(user, opts \\ %{}) do
+ opts = Map.put(opts, "local_only", true)
+ ActivityPub.fetch_public_activities(opts)
+ |> activities_to_statuses(%{for: user})
+ end
+
+ def fetch_public_and_external_statuses(user, opts \\ %{}) do
ActivityPub.fetch_public_activities(opts)
|> activities_to_statuses(%{for: user})
end
end
def fetch_conversation(user, id) do
- query = from activity in Activity,
- where: fragment("? @> ?", activity.data, ^%{ statusnetConversationId: id}),
- limit: 1
-
- with %Activity{} = activity <- Repo.one(query),
- context <- activity.data["context"],
+ with context when is_binary(context) <- conversation_id_to_context(id),
activities <- ActivityPub.fetch_activities_for_context(context),
statuses <- activities |> activities_to_statuses(%{for: user})
do
statuses
- else e ->
- IO.inspect(e)
+ else _e ->
[]
end
end
end
def follow(%User{} = follower, params) do
- with { :ok, %User{} = followed } <- get_user(params),
- { :ok, follower } <- User.follow(follower, followed),
- { :ok, activity } <- ActivityPub.insert(%{
- "type" => "Follow",
- "actor" => follower.ap_id,
- "object" => followed.ap_id,
- "published" => make_date()
- })
+ with {:ok, %User{} = followed} <- get_user(params),
+ {:ok, follower} <- User.follow(follower, followed),
- {:ok, activity} <- ActivityPub.insert(%{
- "type" => "Follow",
- "actor" => follower.ap_id,
- "to" => [followed.ap_id],
- "object" => followed.ap_id,
- "published" => make_date()
- })
++ {:ok, activity} <- ActivityPub.follow(follower, followed)
do
- { :ok, follower, followed, activity }
- # TODO move all this to ActivityPub
- Pleroma.Web.Federator.enqueue(:publish, activity)
+ {:ok, follower, followed, activity}
else
err -> err
end
end
- def unfollow(%User{} = follower, params) do
+ def unfollow(%User{} = follower, params) do
- with {:ok, %User{} = unfollowed} <- get_user(params),
- {:ok, follower} <- User.unfollow(follower, unfollowed)
+ with { :ok, %User{} = unfollowed } <- get_user(params),
+ { :ok, follower, follow_activity } <- User.unfollow(follower, unfollowed),
+ { :ok, _activity } <- ActivityPub.insert(%{
+ "type" => "Undo",
+ "actor" => follower.ap_id,
- "object" => follow_activity, # get latest Follow for these users
++ "object" => follow_activity.data["id"], # get latest Follow for these users
+ "published" => make_date()
+ })
do
- {:ok, follower, unfollowed}
+ { :ok, follower, unfollowed }
else
err -> err
end
Enum.reduce(mentions, text, fn ({match, %User{ap_id: ap_id}}, text) -> String.replace(text, match, "<a href='#{ap_id}'>#{match}</a>") 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 register_user(params) do
params = %{
nickname: params["nickname"],
{:error, changeset} ->
errors = Ecto.Changeset.traverse_errors(changeset, fn {msg, _opts} -> msg end)
|> Poison.encode!
- {:error, %{error: errors}}
+ {:error, %{error: errors}}
end
end
def get_user(user \\ nil, params) do
case params do
- %{ "user_id" => user_id } ->
+ %{"user_id" => user_id} ->
case target = Repo.get(User, user_id) do
nil ->
{:error, "No user with such user_id"}
_ ->
{:ok, target}
end
- %{ "screen_name" => nickname } ->
+ %{"screen_name" => nickname} ->
case target = Repo.get_by(User, nickname: nickname) do
nil ->
{:error, "No user with such screen_name"}
defp make_date do
DateTime.utc_now() |> DateTime.to_iso8601
end
+
+ def context_to_conversation_id(context) do
+ with %Object{id: id} <- Object.get_cached_by_ap_id(context) do
+ id
+ else _e ->
+ changeset = Object.context_mapping(context)
+ {:ok, %{id: id}} = Repo.insert(changeset)
+ id
+ end
+ end
+
+ def conversation_id_to_context(id) do
+ with %Object{data: %{"id" => context}} <- Repo.get(Object, id) do
+ context
+ else _e ->
+ {:error, "No such conversation"}
+ end
+ end
end
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"published_at" => DateTime.utc_now() |> DateTime.to_iso8601,
"likes" => [],
- "like_count" => 0
+ "like_count" => 0,
+ "context" => "2hu"
}
%Pleroma.Object{
"actor" => note.data["actor"],
"to" => note.data["to"],
"object" => note.data,
- "published_at" => DateTime.utc_now() |> DateTime.to_iso8601
+ "published_at" => DateTime.utc_now() |> DateTime.to_iso8601,
+ "context" => note.data["context"]
}
%Pleroma.Activity{
}
end
+ def follow_activity_factory do
+ follower = insert(:user)
+ followed = insert(:user)
+
+ data = %{
+ "id" => Pleroma.Web.ActivityPub.ActivityPub.generate_activity_id,
+ "actor" => follower.ap_id,
+ "type" => "Follow",
+ "object" => followed.ap_id,
+ "published_at" => DateTime.utc_now() |> DateTime.to_iso8601
+ }
+
+ %Pleroma.Activity{
+ data: data
+ }
+ end
+
def websub_subscription_factory do
%Pleroma.Web.Websub.WebsubServerSubscription{
topic: "http://example.org",
state: "requested"
}
end
+
+ def websub_client_subscription_factory do
+ %Pleroma.Web.Websub.WebsubClientSubscription{
+ topic: "http://example.org",
+ secret: "here's a secret",
+ valid_until: nil,
+ state: "requested",
+ subscribers: []
+ }
+ end
end
defmodule Pleroma.UserTest do
alias Pleroma.Builders.UserBuilder
- alias Pleroma.User
+ alias Pleroma.{User, Repo}
+ alias Pleroma.Web.OStatus
+ alias Pleroma.Web.Websub.WebsubClientSubscription
use Pleroma.DataCase
import Pleroma.Factory
+ import Ecto.Query
test "ap_id returns the activity pub id for the user" do
host =
user = UserBuilder.build
- expected_ap_id = "https://#{host}/users/#{user.nickname}"
+ expected_ap_id = "#{Pleroma.Web.base_url}/users/#{user.nickname}"
assert expected_ap_id == User.ap_id(user)
end
user = insert(:user)
followed = insert(:user)
- {:ok, user } = User.follow(user, followed)
+ {:ok, user} = User.follow(user, followed)
user = Repo.get(User, user.id)
assert user.following == [User.ap_followers(followed)]
end
+ test "following a remote user will ensure a websub subscription is present" do
+ user = insert(:user)
+ {:ok, followed} = OStatus.make_user("shp@social.heldscal.la")
+
+ assert followed.local == false
+
+ {:ok, user} = User.follow(user, followed)
+ assert user.following == [User.ap_followers(followed)]
+
+ query = from w in WebsubClientSubscription,
+ where: w.topic == ^followed.info["topic"]
+ websub = Repo.one(query)
+
+ assert websub
+ end
+
test "unfollow takes a user and another user" do
followed = insert(:user)
user = insert(:user, %{following: [User.ap_followers(followed)]})
- {:ok, user } = User.unfollow(user, followed)
+ {:ok, user, _activity } = User.unfollow(user, followed)
user = Repo.get(User, user.id)
assert changeset.changes[:following] == [User.ap_followers(%User{nickname: @full_user_data.nickname})]
end
end
+
+ describe "fetching a user from nickname or trying to build one" do
+ test "gets an existing user" do
+ user = insert(:user)
+ fetched_user = User.get_or_fetch_by_nickname(user.nickname)
+
+ assert user == fetched_user
+ end
+
+ test "fetches an external user via ostatus if no user exists" do
+ fetched_user = User.get_or_fetch_by_nickname("shp@social.heldscal.la")
+ assert fetched_user.nickname == "shp@social.heldscal.la"
+ end
+
+ test "returns nil if no user could be fetched" do
+ fetched_user = User.get_or_fetch_by_nickname("nonexistant@social.heldscal.la")
+ assert fetched_user == nil
+ end
+
+ test "returns nil for nonexistant local user" do
+ fetched_user = User.get_or_fetch_by_nickname("nonexistant")
+ assert fetched_user == nil
+ end
+ end
+
+ test "returns an ap_id for a user" do
+ user = insert(:user)
+ assert User.ap_id(user) == Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :feed_redirect, user.nickname)
+ end
+
+ test "returns an ap_followers link for a user" do
+ user = insert(:user)
+ assert User.ap_followers(user) == Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :feed_redirect, user.nickname) <> "/followers"
+ end
end
+
end
end
+ describe "create activities" do
+ test "removes doubled 'to' recipients" do
+ {:ok, activity} = ActivityPub.create(["user1", "user1", "user2"], %User{ap_id: "1"}, "", %{})
+ assert activity.data["to"] == ["user1", "user2"]
+ end
+ end
+
describe "fetch activities for recipients" do
test "retrieve the activities for certain recipients" do
{:ok, activity_one} = ActivityBuilder.insert(%{"to" => ["someone"]})
assert like_activity.data["type"] == "Like"
assert like_activity.data["object"] == object.data["id"]
assert like_activity.data["to"] == [User.ap_followers(user), note_activity.data["actor"]]
+ assert like_activity.data["context"] == object.data["context"]
assert object.data["like_count"] == 1
assert object.data["likes"] == [user.ap_id]
assert announce_activity.data["to"] == [User.ap_followers(user), note_activity.data["actor"]]
assert announce_activity.data["object"] == object.data["id"]
assert announce_activity.data["actor"] == user.ap_id
+ assert announce_activity.data["context"] == object.data["context"]
end
end
end
end
+ describe "fetch the latest Follow" do
+ test "fetches the latest Follow activity" do
+ %Activity{data: %{"type" => "Follow"}} = activity = insert(:follow_activity)
+ follower = Repo.get_by(User, ap_id: activity.data["actor"])
+ followed = Repo.get_by(User, ap_id: activity.data["object"])
+
+ assert activity == ActivityPub.fetch_latest_follow(follower, followed)
+ end
+ end
+
++ describe "following / unfollowing" do
++ test "creates a follow activity" do
++ follower = insert(:user)
++ followed = insert(:user)
++
++ {:ok, activity} = ActivityPub.follow(follower, followed)
++ assert activity.data["type"] == "Follow"
++ assert activity.data["actor"] == follower.ap_id
++ assert activity.data["object"] == followed.ap_id
++ end
++
++ test "creates an undo activity for the last follow" do
++ follower = insert(:user)
++ followed = insert(:user)
++
++ {:ok, follow_activity} = ActivityPub.follow(follower, followed)
++ {:ok, activity} = ActivityPub.unfollow(follower, followed)
++
++ assert activity.data["type"] == "Undo"
++ assert activity.data["actor"] == follower.ap_id
++ assert activity.data["object"] == follow_activity.data["id"]
++ end
++ end
++
def data_uri do
"data:image/jpeg;base64,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"
end
use Pleroma.DataCase
alias Pleroma.Web.OStatus.ActivityRepresenter
- alias Pleroma.{User, Activity}
+ alias Pleroma.{User, Activity, Object}
+ alias Pleroma.Web.ActivityPub.ActivityPub
import Pleroma.Factory
<content type="html">#{note_activity.data["object"]["content"]}</content>
<published>#{inserted_at}</published>
<updated>#{updated_at}</updated>
+ <ostatus:conversation>#{note_activity.data["context"]}</ostatus:conversation>
+ <link href="#{note_activity.data["context"]}" rel="ostatus:conversation" />
+ <link type="application/atom+xml" href="#{note_activity.data["object"]["id"]}" rel="self" />
+ <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
"""
tuple = ActivityRepresenter.to_simple_form(note_activity, user)
assert clean(res) == clean(expected)
end
+ test "a reply note" do
+ note = insert(:note_activity)
+ answer = insert(:note_activity)
+ object = answer.data["object"]
+ object = Map.put(object, "inReplyTo", note.data["object"]["id"])
+
+ data = %{answer.data | "object" => object}
+ answer = %{answer | data: data}
+
+ updated_at = answer.updated_at
+ |> NaiveDateTime.to_iso8601
+ inserted_at = answer.inserted_at
+ |> NaiveDateTime.to_iso8601
+
+ user = User.get_cached_by_ap_id(answer.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>#{answer.data["object"]["id"]}</id>
+ <title>New note by #{user.nickname}</title>
+ <content type="html">#{answer.data["object"]["content"]}</content>
+ <published>#{inserted_at}</published>
+ <updated>#{updated_at}</updated>
+ <ostatus:conversation>#{answer.data["context"]}</ostatus:conversation>
+ <link href="#{answer.data["context"]}" rel="ostatus:conversation" />
+ <link type="application/atom+xml" href="#{answer.data["object"]["id"]}" rel="self" />
+ <thr:in-reply-to ref="#{note.data["object"]["id"]}" />
+ <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
+ """
+
+ tuple = ActivityRepresenter.to_simple_form(answer, user)
+
+ res = :xmerl.export_simple_content(tuple, :xmerl_xml) |> IO.iodata_to_binary
+
+ assert clean(res) == clean(expected)
+ end
+
+ test "an announce activity" do
+ note = insert(:note_activity)
+ user = insert(:user)
+ object = Object.get_cached_by_ap_id(note.data["object"]["id"])
+
+ {:ok, announce, object} = ActivityPub.announce(user, object)
+
+ announce = Repo.get(Activity, announce.id)
+
+ note_user = User.get_cached_by_ap_id(note.data["actor"])
+ note = Repo.get(Activity, note.id)
+ note_xml = ActivityRepresenter.to_simple_form(note, note_user, true)
+ |> :xmerl.export_simple_content(:xmerl_xml)
+ |> to_string
+
+ updated_at = announce.updated_at
+ |> NaiveDateTime.to_iso8601
+ inserted_at = announce.inserted_at
+ |> NaiveDateTime.to_iso8601
+
+ expected = """
+ <activity:object-type>http://activitystrea.ms/schema/1.0/activity</activity:object-type>
+ <activity:verb>http://activitystrea.ms/schema/1.0/share</activity:verb>
+ <id>#{announce.data["id"]}</id>
+ <title>#{user.nickname} repeated a notice</title>
+ <content type="html">RT #{note.data["object"]["content"]}</content>
+ <published>#{inserted_at}</published>
+ <updated>#{updated_at}</updated>
+ <ostatus:conversation>#{announce.data["context"]}</ostatus:conversation>
+ <link href="#{announce.data["context"]}" rel="ostatus:conversation" />
+ <link rel="self" type="application/atom+xml" href="#{announce.data["id"]}"/>
+ <activity:object>
+ #{note_xml}
+ </activity:object>
+ <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="#{note.data["actor"]}"/>
+ """
+
+ announce_xml = ActivityRepresenter.to_simple_form(announce, user)
+ |> :xmerl.export_simple_content(:xmerl_xml)
+ |> to_string
+
+ assert clean(expected) == clean(announce_xml)
+ end
+
+ test "a like activity" do
+ note = insert(:note)
+ user = insert(:user)
+ {:ok, like, _note} = ActivityPub.like(user, note)
+
+ # TODO: Are these the correct dates?
+ updated_at = like.updated_at
+ |> NaiveDateTime.to_iso8601
+ inserted_at = like.inserted_at
+ |> NaiveDateTime.to_iso8601
+
+ tuple = ActivityRepresenter.to_simple_form(like, user)
+ refute is_nil(tuple)
+
+ res = :xmerl.export_simple_content(tuple, :xmerl_xml) |> IO.iodata_to_binary
+
+ expected = """
+ <activity:verb>http://activitystrea.ms/schema/1.0/favorite</activity:verb>
+ <id>#{like.data["id"]}</id>
+ <title>New favorite by #{user.nickname}</title>
+ <content type="html">#{user.nickname} favorited something</content>
+ <published>#{inserted_at}</published>
+ <updated>#{updated_at}</updated>
+ <activity:object>
+ <activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
+ <id>#{note.data["id"]}</id>
+ </activity:object>
+ <ostatus:conversation>#{like.data["context"]}</ostatus:conversation>
+ <link href="#{like.data["context"]}" rel="ostatus:conversation" />
+ <link rel="self" type="application/atom+xml" href="#{like.data["id"]}"/>
+ <thr:in-reply-to ref="#{note.data["id"]}" />
+ <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="#{note.data["actor"]}"/>
+ """
+
+ assert clean(res) == clean(expected)
+ end
+
+ test "a follow activity" do
+ follower = insert(:user)
+ followed = insert(:user)
+ {:ok, activity} = ActivityPub.insert(%{
+ "type" => "Follow",
+ "actor" => follower.ap_id,
+ "object" => followed.ap_id,
+ "to" => [followed.ap_id]
+ })
+
+
+ # TODO: Are these the correct dates?
+ updated_at = activity.updated_at
+ |> NaiveDateTime.to_iso8601
+ inserted_at = activity.inserted_at
+ |> NaiveDateTime.to_iso8601
+
+ tuple = ActivityRepresenter.to_simple_form(activity, follower)
+
+ refute is_nil(tuple)
+
+ res = :xmerl.export_simple_content(tuple, :xmerl_xml) |> IO.iodata_to_binary
+
+ expected = """
+ <activity:object-type>http://activitystrea.ms/schema/1.0/activity</activity:object-type>
+ <activity:verb>http://activitystrea.ms/schema/1.0/follow</activity:verb>
+ <id>#{activity.data["id"]}</id>
+ <title>#{follower.nickname} started following #{activity.data["object"]}</title>
+ <content type="html"> #{follower.nickname} started following #{activity.data["object"]}</content>
+ <published>#{inserted_at}</published>
+ <updated>#{updated_at}</updated>
+ <activity:object>
+ <activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
+ <id>#{activity.data["object"]}</id>
+ <uri>#{activity.data["object"]}</uri>
+ </activity:object>
+ <link rel="self" type="application/atom+xml" href="#{activity.data["id"]}"/>
+ <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="#{activity.data["object"]}"/>
+ """
+
+ assert clean(res) == clean(expected)
+ end
+
++ test "an unfollow activity" do
++ follower = insert(:user)
++ followed = insert(:user)
++ {:ok, _activity} = ActivityPub.follow(follower, followed)
++ {:ok, activity} = ActivityPub.unfollow(follower, followed)
++
++ # TODO: Are these the correct dates?
++ updated_at = activity.updated_at
++ |> NaiveDateTime.to_iso8601
++ inserted_at = activity.inserted_at
++ |> NaiveDateTime.to_iso8601
++
++ tuple = ActivityRepresenter.to_simple_form(activity, follower)
++
++ refute is_nil(tuple)
++
++ res = :xmerl.export_simple_content(tuple, :xmerl_xml) |> IO.iodata_to_binary
++
++ expected = """
++ <activity:object-type>http://activitystrea.ms/schema/1.0/activity</activity:object-type>
++ <activity:verb>http://activitystrea.ms/schema/1.0/unfollow</activity:verb>
++ <id>#{activity.data["id"]}</id>
++ <title>#{follower.nickname} stopped following #{followed.ap_id}</title>
++ <content type="html"> #{follower.nickname} stopped following #{followed.ap_id}</content>
++ <published>#{inserted_at}</published>
++ <updated>#{updated_at}</updated>
++ <activity:object>
++ <activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
++ <id>#{followed.ap_id}</id>
++ <uri>#{followed.ap_id}</uri>
++ </activity:object>
++ <link rel="self" type="application/atom+xml" href="#{activity.data["id"]}"/>
++ <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="#{followed.ap_id}"/>
++ """
++
++ assert clean(res) == clean(expected)
++ end
++
test "an unknown activity" do
tuple = ActivityRepresenter.to_simple_form(%Activity{}, nil)
assert is_nil(tuple)
describe "GET /statusnet/conversation/:id.json" do
test "returns the statuses in the conversation", %{conn: conn} do
{:ok, _user} = UserBuilder.insert
- {:ok, _activity} = ActivityBuilder.insert(%{"statusnetConversationId" => 1, "context" => "2hu"})
- {:ok, _activity_two} = ActivityBuilder.insert(%{"statusnetConversationId" => 1,"context" => "2hu"})
+ {:ok, _activity} = ActivityBuilder.insert(%{"context" => "2hu"})
+ {:ok, _activity_two} = ActivityBuilder.insert(%{"context" => "2hu"})
{:ok, _activity_three} = ActivityBuilder.insert(%{"context" => "3hu"})
+ {:ok, object} = Object.context_mapping("2hu") |> Repo.insert
conn = conn
- |> get("/api/statusnet/conversation/1.json")
+ |> get("/api/statusnet/conversation/#{object.id}.json")
response = json_response(conn, 200)
{:ok, current_user} = User.follow(current_user, followed)
assert current_user.following == [User.ap_followers(followed)]
++ ActivityPub.follow(current_user, followed)
conn = conn
|> with_credentials(current_user.nickname, "test")
header_content = "Basic " <> Base.encode64("#{username}:#{password}")
put_req_header(conn, "authorization", header_content)
end
--
-- setup do
-- Supervisor.terminate_child(Pleroma.Supervisor, ConCache)
-- Supervisor.restart_child(Pleroma.Supervisor, ConCache)
-- :ok
-- end
end
{ :ok, activity = %Activity{} } = TwitterAPI.create_status(user, input)
- assert get_in(activity.data, ["object", "content"]) == "Hello again, <a href='shp'>@shp</a>.<br>This is on another line."
+ assert get_in(activity.data, ["object", "content"]) == "Hello again, <a href='shp'>@shp</a>.<br>This is on another line.<br><a href='http://example.org/image.jpg'>http://example.org/image.jpg</a>"
assert get_in(activity.data, ["object", "type"]) == "Note"
assert get_in(activity.data, ["object", "actor"]) == user.ap_id
assert get_in(activity.data, ["actor"]) == user.ap_id
assert Enum.member?(get_in(activity.data, ["to"]), User.ap_followers(user))
assert Enum.member?(get_in(activity.data, ["to"]), "https://www.w3.org/ns/activitystreams#Public")
assert Enum.member?(get_in(activity.data, ["to"]), "shp")
+ assert activity.local == true
- # Add a context + 'statusnet_conversation_id'
+ # Add a context
assert is_binary(get_in(activity.data, ["context"]))
assert is_binary(get_in(activity.data, ["object", "context"]))
- assert get_in(activity.data, ["object", "statusnetConversationId"]) == activity.id
- assert get_in(activity.data, ["statusnetConversationId"]) == activity.id
assert is_list(activity.data["object"]["attachment"])
assert get_in(reply.data, ["context"]) == get_in(activity.data, ["context"])
assert get_in(reply.data, ["object", "context"]) == get_in(activity.data, ["object", "context"])
- assert get_in(reply.data, ["statusnetConversationId"]) == get_in(activity.data, ["statusnetConversationId"])
- assert get_in(reply.data, ["object", "statusnetConversationId"]) == get_in(activity.data, ["object", "statusnetConversationId"])
assert get_in(reply.data, ["object", "inReplyTo"]) == get_in(activity.data, ["object", "id"])
assert get_in(reply.data, ["object", "inReplyToStatusId"]) == activity.id
assert Enum.member?(get_in(reply.data, ["to"]), "some_cool_id")
end
- test "fetch public statuses" do
+ test "fetch public statuses, excluding remote ones." do
%{ public: activity, user: user } = ActivityBuilder.public_and_non_public
+ insert(:note_activity, %{local: false})
follower = insert(:user, following: [User.ap_followers(user)])
assert Enum.at(statuses, 0) == ActivityRepresenter.to_map(activity, %{user: user, for: follower})
end
+ test "fetch whole known network statuses" do
+ %{ public: activity, user: user } = ActivityBuilder.public_and_non_public
+ insert(:note_activity, %{local: false})
+
+ follower = insert(:user, following: [User.ap_followers(user)])
+
+ statuses = TwitterAPI.fetch_public_and_external_statuses(follower)
+
+ assert length(statuses) == 2
+ assert Enum.at(statuses, 0) == ActivityRepresenter.to_map(activity, %{user: user, for: follower})
+ end
+
test "fetch friends' statuses" do
user = insert(:user, %{following: ["someguy/followers"]})
{:ok, activity} = ActivityBuilder.insert(%{"to" => ["someguy/followers"]})
test "Unfollow another user using user_id" do
unfollowed = insert(:user)
user = insert(:user, %{following: [User.ap_followers(unfollowed)]})
++ ActivityPub.follow(user, unfollowed)
{:ok, user, unfollowed } = TwitterAPI.unfollow(user, %{"user_id" => unfollowed.id})
assert user.following == []
unfollowed = insert(:user)
user = insert(:user, %{following: [User.ap_followers(unfollowed)]})
++ ActivityPub.follow(user, unfollowed)
++
{:ok, user, unfollowed } = TwitterAPI.unfollow(user, %{"screen_name" => unfollowed.nickname})
assert user.following == []
test "fetch statuses in a context using the conversation id" do
{:ok, user} = UserBuilder.insert()
- {:ok, activity} = ActivityBuilder.insert(%{"statusnetConversationId" => 1, "context" => "2hu"})
- {:ok, activity_two} = ActivityBuilder.insert(%{"statusnetConversationId" => 1,"context" => "2hu"})
+ {:ok, activity} = ActivityBuilder.insert(%{"context" => "2hu"})
+ {:ok, activity_two} = ActivityBuilder.insert(%{"context" => "2hu"})
{:ok, _activity_three} = ActivityBuilder.insert(%{"context" => "3hu"})
- statuses = TwitterAPI.fetch_conversation(user, 1)
+ {:ok, object} = Object.context_mapping("2hu") |> Repo.insert
+
+ statuses = TwitterAPI.fetch_conversation(user, object.id)
assert length(statuses) == 2
assert Enum.at(statuses, 0)["id"] == activity.id
refute Repo.get_by(User, nickname: "lain")
end
+ test "it assigns an integer conversation_id" do
+ note_activity = insert(:note_activity)
+ user = User.get_cached_by_ap_id(note_activity.data["actor"])
+ status = ActivityRepresenter.to_map(note_activity, %{user: user})
+
+ assert is_number(status["statusnet_conversation_id"])
+ end
+
setup do
Supervisor.terminate_child(Pleroma.Supervisor, Cachex)
Supervisor.restart_child(Pleroma.Supervisor, Cachex)
:ok
end
+
+ describe "context_to_conversation_id" do
+ test "creates a mapping object" do
+ conversation_id = TwitterAPI.context_to_conversation_id("random context")
+ object = Object.get_by_ap_id("random context")
+
+ assert conversation_id == object.id
+ end
+
+ test "returns an existing mapping for an existing object" do
+ {:ok, object} = Object.context_mapping("random context") |> Repo.insert
+ conversation_id = TwitterAPI.context_to_conversation_id("random context")
+
+ assert conversation_id == object.id
+ end
+ end
end