Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85823822
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Authored By
Unknown
Size
17 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/lib/pleroma/web/twitter_api/representers/activity_representer.ex b/lib/pleroma/web/twitter_api/representers/activity_representer.ex
index 0cf20dc45..32c6a48e9 100644
--- a/lib/pleroma/web/twitter_api/representers/activity_representer.ex
+++ b/lib/pleroma/web/twitter_api/representers/activity_representer.ex
@@ -1,23 +1,32 @@
defmodule Pleroma.Web.TwitterAPI.Representers.ActivityRepresenter do
use Pleroma.Web.TwitterAPI.Representers.BaseRepresenter
alias Pleroma.Web.TwitterAPI.Representers.{UserRepresenter, ObjectRepresenter}
alias Pleroma.Activity
def to_map(%Activity{} = activity, %{user: user} = opts) do
content = get_in(activity.data, ["object", "content"])
published = get_in(activity.data, ["object", "published"])
+
+ mentions = opts[:mentioned] || []
+
+ attentions = activity.data["to"]
+ |> Enum.map(fn (ap_id) -> Enum.find(mentions, fn(user) -> ap_id == user.ap_id end) end)
+ |> Enum.filter(&(&1))
+ |> Enum.map(fn (user) -> UserRepresenter.to_map(user, opts) end)
+
%{
"id" => activity.id,
"user" => UserRepresenter.to_map(user, opts),
"attentions" => [],
"statusnet_html" => content,
"text" => content,
"is_local" => true,
"is_post_verb" => true,
"created_at" => published,
"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)
+ "attachments" => (activity.data["object"]["attachment"] || []) |> ObjectRepresenter.enum_to_list(opts),
+ "attentions" => attentions
}
end
end
diff --git a/lib/pleroma/web/twitter_api/twitter_api.ex b/lib/pleroma/web/twitter_api/twitter_api.ex
index c07c7cfbf..2aaa73b78 100644
--- a/lib/pleroma/web/twitter_api/twitter_api.ex
+++ b/lib/pleroma/web/twitter_api/twitter_api.ex
@@ -1,149 +1,181 @@
defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
alias Pleroma.{User, Activity, Repo, Object}
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.TwitterAPI.Representers.ActivityRepresenter
import Ecto.Query
def create_status(user = %User{}, data = %{}) do
date = DateTime.utc_now() |> DateTime.to_iso8601
attachments = Enum.map(data["media_ids"] || [], fn (media_id) ->
Repo.get(Object, media_id).data
end)
context = ActivityPub.generate_context_id
+
+ content = HtmlSanitizeEx.strip_tags(data["status"])
+
+ mentions = parse_mentions(content)
+
+ default_to = [
+ User.ap_followers(user),
+ "https://www.w3.org/ns/activitystreams#Public"
+ ]
+
+ to = default_to ++ Enum.map(mentions, fn ({_, %{ap_id: ap_id}}) -> ap_id end)
+
+ content_html = add_user_links(content, mentions)
+
activity = %{
"type" => "Create",
- "to" => [
- User.ap_followers(user),
- "https://www.w3.org/ns/activitystreams#Public"
- ],
- "actor" => User.ap_id(user),
+ "to" => to,
+ "actor" => user.ap_id,
"object" => %{
"type" => "Note",
- "content" => data["status"],
+ "to" => to,
+ "content" => content_html,
"published" => date,
"context" => context,
"attachment" => attachments
},
"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
add_conversation_id(activity)
end
end
def fetch_friend_statuses(user, opts \\ %{}) do
ActivityPub.fetch_activities(user.following, opts)
|> activities_to_statuses(%{for: user})
end
def fetch_public_statuses(user, opts \\ %{}) do
ActivityPub.fetch_public_activities(opts)
|> activities_to_statuses(%{for: user})
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"],
activities <- ActivityPub.fetch_activities_for_context(context),
statuses <- activities |> activities_to_statuses(%{for: user})
do
statuses
else e ->
- IO.inspect(e)
+ IO.inspect(e)
[]
end
end
def fetch_status(user, id) do
with %Activity{} = activity <- Repo.get(Activity, id) do
activity_to_status(activity, %{for: user})
end
end
def follow(%User{} = follower, followed_id) do
with %User{} = followed <- Repo.get(User, followed_id),
{ :ok, follower } <- User.follow(follower, followed)
do
{ :ok, follower, followed }
end
end
def unfollow(%User{} = follower, followed_id) do
with %User{} = followed <- Repo.get(User, followed_id),
{ :ok, follower } <- User.unfollow(follower, followed)
do
{ :ok, follower, followed }
end
end
def upload(%Plug.Upload{} = file) do
{:ok, object} = ActivityPub.upload(file)
url = List.first(object.data["url"])
href = url["href"]
type = url["mediaType"]
# Fake this as good as possible...
"""
<?xml version="1.0" encoding="UTF-8"?>
<rsp stat="ok" xmlns:atom="http://www.w3.org/2005/Atom">
<mediaid>#{object.id}</mediaid>
<media_id>#{object.id}</media_id>
<media_id_string>#{object.id}</media_id_string>
<media_url>#{href}</media_url>
<mediaurl>#{href}</mediaurl>
<atom:link rel="enclosure" href="#{href}" type="#{type}"></atom:link>
</rsp>
"""
end
+ def parse_mentions(text) do
+ # Modified from https://www.w3.org/TR/html5/forms.html#valid-e-mail-address
+ regex = ~r/@[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@?[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*/
+
+ Regex.scan(regex, text)
+ |> List.flatten
+ |> Enum.uniq
+ |> Enum.map(fn ("@" <> match = full_match) -> {full_match, Repo.get_by(User, nickname: match)} end)
+ |> Enum.filter(fn ({_match, user}) -> user end)
+ end
+
+ def add_user_links(text, mentions) do
+ 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)
changeset = Ecto.Changeset.change(activity, data: data)
Repo.update(changeset)
end
end
defp activities_to_statuses(activities, opts) do
Enum.map(activities, fn(activity) ->
activity_to_status(activity, opts)
end)
end
defp activity_to_status(activity, opts) do
actor = get_in(activity.data, ["actor"])
user = Repo.get_by!(User, ap_id: actor)
- ActivityRepresenter.to_map(activity, Map.merge(opts, %{user: user}))
+ mentioned_users = Repo.all(from user in User, where: user.ap_id in ^activity.data["to"])
+ ActivityRepresenter.to_map(activity, Map.merge(opts, %{user: user, mentioned: mentioned_users}))
end
end
diff --git a/test/web/twitter_api/representers/activity_representer_test.exs b/test/web/twitter_api/representers/activity_representer_test.exs
index 91300f229..256d920c0 100644
--- a/test/web/twitter_api/representers/activity_representer_test.exs
+++ b/test/web/twitter_api/representers/activity_representer_test.exs
@@ -1,70 +1,75 @@
defmodule Pleroma.Web.TwitterAPI.Representers.ActivityRepresenterTest do
use Pleroma.DataCase
alias Pleroma.{User, Activity, Object}
alias Pleroma.Web.TwitterAPI.Representers.{UserRepresenter, ActivityRepresenter, ObjectRepresenter}
alias Pleroma.Builders.UserBuilder
test "an activity" do
{:ok, user} = UserBuilder.insert
+ {:ok, mentioned_user } = UserBuilder.insert(%{nickname: "shp", ap_id: "shp"})
{:ok, follower} = UserBuilder.insert(%{following: [User.ap_followers(user)]})
object = %Object{
data: %{
"type" => "Image",
"url" => [
%{
"type" => "Link",
"mediaType" => "image/jpg",
"href" => "http://example.org/image.jpg"
}
],
"uuid" => 1
}
}
- content = "Some content"
+ content = "Some content mentioning @shp"
date = DateTime.utc_now() |> DateTime.to_iso8601
activity = %Activity{
id: 1,
data: %{
"type" => "Create",
"to" => [
User.ap_followers(user),
- "https://www.w3.org/ns/activitystreams#Public"
+ "https://www.w3.org/ns/activitystreams#Public",
+ mentioned_user.ap_id
],
"actor" => User.ap_id(user),
"object" => %{
"published" => date,
"type" => "Note",
"content" => content,
"inReplyToStatusId" => 213123,
"statusnetConversationId" => 4711,
"attachment" => [
object
]
},
"published" => date
}
}
expected_status = %{
"id" => activity.id,
"user" => UserRepresenter.to_map(user, %{for: follower}),
"is_local" => true,
"attentions" => [],
"statusnet_html" => content,
"text" => content,
"is_post_verb" => true,
"created_at" => date,
"in_reply_to_status_id" => 213123,
"statusnet_conversation_id" => 4711,
"attachments" => [
ObjectRepresenter.to_map(object)
+ ],
+ "attentions" => [
+ UserRepresenter.to_map(mentioned_user, %{for: follower})
]
}
- assert ActivityRepresenter.to_map(activity, %{user: user, for: follower}) == expected_status
+ assert ActivityRepresenter.to_map(activity, %{user: user, for: follower, mentioned: [mentioned_user]}) == expected_status
end
end
diff --git a/test/web/twitter_api/twitter_api_test.exs b/test/web/twitter_api/twitter_api_test.exs
index 8d123ff4e..99b6a6cb2 100644
--- a/test/web/twitter_api/twitter_api_test.exs
+++ b/test/web/twitter_api/twitter_api_test.exs
@@ -1,144 +1,174 @@
defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
use Pleroma.DataCase
alias Pleroma.Builders.{UserBuilder, ActivityBuilder}
alias Pleroma.Web.TwitterAPI.TwitterAPI
alias Pleroma.{Activity, User, Object, Repo}
alias Pleroma.Web.TwitterAPI.Representers.ActivityRepresenter
test "create a status" do
- user = UserBuilder.build
+ user = UserBuilder.build(%{ap_id: "142344"})
+ _mentioned_user = UserBuilder.insert(%{nickname: "shp", ap_id: "shp"})
+
object_data = %{
"type" => "Image",
"url" => [
%{
"type" => "Link",
"mediaType" => "image/jpg",
"href" => "http://example.org/image.jpg"
}
],
"uuid" => 1
}
object = Repo.insert!(%Object{data: object_data})
input = %{
- "status" => "Hello again.",
+ "status" => "Hello again, @shp.<script></script>",
"media_ids" => [object.id]
}
{ :ok, activity = %Activity{} } = TwitterAPI.create_status(user, input)
- assert get_in(activity.data, ["object", "content"]) == "Hello again."
+ assert get_in(activity.data, ["object", "content"]) == "Hello again, <a href='shp'>@shp</a>."
assert get_in(activity.data, ["object", "type"]) == "Note"
- assert get_in(activity.data, ["actor"]) == User.ap_id(user)
+ 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")
# Add a context + 'statusnet_conversation_id'
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"])
end
test "create a status that is a reply" do
- user = UserBuilder.build
+ user = UserBuilder.build(%{ap_id: "some_cool_id"})
input = %{
"status" => "Hello again."
}
{ :ok, activity = %Activity{} } = TwitterAPI.create_status(user, input)
input = %{
"status" => "Here's your (you).",
"in_reply_to_status_id" => activity.id
}
{ :ok, reply = %Activity{} } = TwitterAPI.create_status(user, input)
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
%{ public: activity, user: user } = ActivityBuilder.public_and_non_public
{:ok, follower } = UserBuilder.insert(%{name: "dude", ap_id: "idididid", following: [User.ap_followers(user)]})
statuses = TwitterAPI.fetch_public_statuses(follower)
assert length(statuses) == 1
assert Enum.at(statuses, 0) == ActivityRepresenter.to_map(activity, %{user: user, for: follower})
end
test "fetch friends' statuses" do
ActivityBuilder.public_and_non_public
{:ok, activity} = ActivityBuilder.insert(%{"to" => ["someguy/followers"]})
{:ok, user} = UserBuilder.insert(%{ap_id: "some other id", following: ["someguy/followers"]})
statuses = TwitterAPI.fetch_friend_statuses(user)
activity_user = Repo.get_by(User, ap_id: activity.data["actor"])
assert length(statuses) == 1
assert Enum.at(statuses, 0) == ActivityRepresenter.to_map(activity, %{user: activity_user})
end
test "fetch a single status" do
{:ok, activity} = ActivityBuilder.insert()
{:ok, user} = UserBuilder.insert()
actor = Repo.get_by!(User, ap_id: activity.data["actor"])
status = TwitterAPI.fetch_status(user, activity.id)
assert status == ActivityRepresenter.to_map(activity, %{for: user, user: actor})
end
test "Follow another user" do
{ :ok, user } = UserBuilder.insert
{ :ok, following } = UserBuilder.insert(%{nickname: "guy"})
{:ok, user, following } = TwitterAPI.follow(user, following.id)
user = Repo.get(User, user.id)
assert user.following == [User.ap_followers(following)]
end
test "Unfollow another user" do
{ :ok, following } = UserBuilder.insert(%{nickname: "guy"})
{ :ok, user } = UserBuilder.insert(%{following: [User.ap_followers(following)]})
{:ok, user, _following } = TwitterAPI.unfollow(user, following.id)
user = Repo.get(User, user.id)
assert user.following == []
end
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_three} = ActivityBuilder.insert(%{"context" => "3hu"})
statuses = TwitterAPI.fetch_conversation(user, 1)
assert length(statuses) == 2
assert Enum.at(statuses, 0)["id"] == activity.id
assert Enum.at(statuses, 1)["id"] == activity_two.id
end
test "upload a file" do
file = %Plug.Upload{content_type: "image/jpg", path: Path.absname("test/fixtures/image.jpg"), filename: "an_image.jpg"}
response = TwitterAPI.upload(file)
assert is_binary(response)
end
+
+ test "it can parse mentions and return the relevant users" do
+ text = "@gsimg According to @archaeme , that is @daggsy."
+
+ {:ok, gsimg} = UserBuilder.insert(%{nickname: "gsimg"})
+ {:ok, archaeme} = UserBuilder.insert(%{nickname: "archaeme"})
+
+ expected_result = [
+ {"@gsimg", gsimg},
+ {"@archaeme", archaeme}
+ ]
+
+ assert TwitterAPI.parse_mentions(text) == expected_result
+ end
+
+ test "it adds user links to an existing text" do
+ text = "@gsimg According to @archaeme , that is @daggsy."
+
+ {:ok, _gsimg} = UserBuilder.insert(%{nickname: "gsimg", ap_id: "first_link" })
+ {:ok, _archaeme} = UserBuilder.insert(%{nickname: "archaeme", ap_id: "second_link"})
+
+ mentions = TwitterAPI.parse_mentions(text)
+ expected_text = "<a href='first_link'>@gsimg</a> According to <a href='second_link'>@archaeme</a> , that is @daggsy."
+
+ assert TwitterAPI.add_user_links(text, mentions) == expected_text
+ end
end
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Thu, Oct 29, 9:28 AM (19 h, 32 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1797864
Default Alt Text
(17 KB)
Attached To
Mode
rPUBE pleroma-upstream
Attached
Detach File
Event Timeline