forked from AkkomaGang/akkoma
Merge branch 'bugfix/no-cc-mentions' into 'develop'
align to/cc addressing pattern with friendica, hubzilla instead of mastodon Closes #341 See merge request pleroma/pleroma!436
This commit is contained in:
commit
b4bd5e40e4
7 changed files with 206 additions and 40 deletions
|
@ -1,6 +1,6 @@
|
||||||
defmodule Pleroma.Notification do
|
defmodule Pleroma.Notification do
|
||||||
use Ecto.Schema
|
use Ecto.Schema
|
||||||
alias Pleroma.{User, Activity, Notification, Repo}
|
alias Pleroma.{User, Activity, Notification, Repo, Object}
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
|
|
||||||
schema "notifications" do
|
schema "notifications" do
|
||||||
|
@ -95,7 +95,7 @@ def dismiss(%{id: user_id} = _user, id) do
|
||||||
|
|
||||||
def create_notifications(%Activity{id: _, data: %{"to" => _, "type" => type}} = activity)
|
def create_notifications(%Activity{id: _, data: %{"to" => _, "type" => type}} = activity)
|
||||||
when type in ["Create", "Like", "Announce", "Follow"] do
|
when type in ["Create", "Like", "Announce", "Follow"] do
|
||||||
users = User.get_notified_from_activity(activity)
|
users = get_notified_from_activity(activity)
|
||||||
|
|
||||||
notifications = Enum.map(users, fn user -> create_notification(activity, user) end)
|
notifications = Enum.map(users, fn user -> create_notification(activity, user) end)
|
||||||
{:ok, notifications}
|
{:ok, notifications}
|
||||||
|
@ -113,4 +113,64 @@ def create_notification(%Activity{} = activity, %User{} = user) do
|
||||||
notification
|
notification
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def get_notified_from_activity(activity, local_only \\ true)
|
||||||
|
|
||||||
|
def get_notified_from_activity(
|
||||||
|
%Activity{data: %{"to" => _, "type" => type} = data} = activity,
|
||||||
|
local_only
|
||||||
|
)
|
||||||
|
when type in ["Create", "Like", "Announce", "Follow"] do
|
||||||
|
recipients =
|
||||||
|
[]
|
||||||
|
|> maybe_notify_to_recipients(activity)
|
||||||
|
|> maybe_notify_mentioned_recipients(activity)
|
||||||
|
|> Enum.uniq()
|
||||||
|
|
||||||
|
User.get_users_from_set(recipients, local_only)
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_notified_from_activity(_, local_only), do: []
|
||||||
|
|
||||||
|
defp maybe_notify_to_recipients(
|
||||||
|
recipients,
|
||||||
|
%Activity{data: %{"to" => to, "type" => type}} = activity
|
||||||
|
) do
|
||||||
|
recipients ++ to
|
||||||
|
end
|
||||||
|
|
||||||
|
defp maybe_notify_mentioned_recipients(
|
||||||
|
recipients,
|
||||||
|
%Activity{data: %{"to" => to, "type" => type} = data} = activity
|
||||||
|
)
|
||||||
|
when type == "Create" do
|
||||||
|
object = Object.normalize(data["object"])
|
||||||
|
|
||||||
|
object_data =
|
||||||
|
cond do
|
||||||
|
!is_nil(object) ->
|
||||||
|
object.data
|
||||||
|
|
||||||
|
is_map(data["object"]) ->
|
||||||
|
data["object"]
|
||||||
|
|
||||||
|
true ->
|
||||||
|
%{}
|
||||||
|
end
|
||||||
|
|
||||||
|
tagged_mentions = maybe_extract_mentions(object_data)
|
||||||
|
|
||||||
|
recipients ++ tagged_mentions
|
||||||
|
end
|
||||||
|
|
||||||
|
defp maybe_notify_mentioned_recipients(recipients, _), do: recipients
|
||||||
|
|
||||||
|
defp maybe_extract_mentions(%{"tag" => tag}) do
|
||||||
|
tag
|
||||||
|
|> Enum.filter(fn x -> is_map(x) end)
|
||||||
|
|> Enum.filter(fn x -> x["type"] == "Mention" end)
|
||||||
|
|> Enum.map(fn x -> x["href"] end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp maybe_extract_mentions(_), do: []
|
||||||
end
|
end
|
||||||
|
|
|
@ -464,36 +464,25 @@ def update_follower_count(%User{} = user) do
|
||||||
update_and_set_cache(cs)
|
update_and_set_cache(cs)
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_notified_from_activity_query(to) do
|
def get_users_from_set_query(ap_ids, false) do
|
||||||
from(
|
from(
|
||||||
u in User,
|
u in User,
|
||||||
where: u.ap_id in ^to,
|
where: u.ap_id in ^ap_ids
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_users_from_set_query(ap_ids, true) do
|
||||||
|
query = get_users_from_set_query(ap_ids, false)
|
||||||
|
|
||||||
|
from(
|
||||||
|
u in query,
|
||||||
where: u.local == true
|
where: u.local == true
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_notified_from_activity(%Activity{recipients: to, data: %{"type" => "Announce"} = data}) do
|
def get_users_from_set(ap_ids, local_only \\ true) do
|
||||||
object = Object.normalize(data["object"])
|
get_users_from_set_query(ap_ids, local_only)
|
||||||
actor = User.get_cached_by_ap_id(data["actor"])
|
|> Repo.all()
|
||||||
|
|
||||||
# ensure that the actor who published the announced object appears only once
|
|
||||||
to =
|
|
||||||
if actor.nickname != nil do
|
|
||||||
to ++ [object.data["actor"]]
|
|
||||||
else
|
|
||||||
to
|
|
||||||
end
|
|
||||||
|> Enum.uniq()
|
|
||||||
|
|
||||||
query = get_notified_from_activity_query(to)
|
|
||||||
|
|
||||||
Repo.all(query)
|
|
||||||
end
|
|
||||||
|
|
||||||
def get_notified_from_activity(%Activity{recipients: to}) do
|
|
||||||
query = get_notified_from_activity_query(to)
|
|
||||||
|
|
||||||
Repo.all(query)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_recipients_from_activity(%Activity{recipients: to}) do
|
def get_recipients_from_activity(%Activity{recipients: to}) do
|
||||||
|
|
|
@ -693,12 +693,9 @@ def add_hashtags(object) do
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_mention_tags(object) do
|
def add_mention_tags(object) do
|
||||||
recipients = object["to"] ++ (object["cc"] || [])
|
|
||||||
|
|
||||||
mentions =
|
mentions =
|
||||||
recipients
|
object
|
||||||
|> Enum.map(fn ap_id -> User.get_cached_by_ap_id(ap_id) end)
|
|> Utils.get_notified_from_object()
|
||||||
|> Enum.filter(& &1)
|
|
||||||
|> Enum.map(fn user ->
|
|> Enum.map(fn user ->
|
||||||
%{"type" => "Mention", "href" => user.ap_id, "name" => "@#{user.nickname}"}
|
%{"type" => "Mention", "href" => user.ap_id, "name" => "@#{user.nickname}"}
|
||||||
end)
|
end)
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
defmodule Pleroma.Web.ActivityPub.Utils do
|
defmodule Pleroma.Web.ActivityPub.Utils do
|
||||||
alias Pleroma.{Repo, Web, Object, Activity, User}
|
alias Pleroma.{Repo, Web, Object, Activity, User, Notification}
|
||||||
alias Pleroma.Web.Router.Helpers
|
alias Pleroma.Web.Router.Helpers
|
||||||
alias Pleroma.Web.Endpoint
|
alias Pleroma.Web.Endpoint
|
||||||
alias Ecto.{Changeset, UUID}
|
alias Ecto.{Changeset, UUID}
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
|
@supported_object_types ["Article", "Note", "Video", "Page"]
|
||||||
|
|
||||||
# Some implementations send the actor URI as the actor field, others send the entire actor object,
|
# Some implementations send the actor URI as the actor field, others send the entire actor object,
|
||||||
# so figure out what the actor's URI is based on what we have.
|
# so figure out what the actor's URI is based on what we have.
|
||||||
def get_ap_id(object) do
|
def get_ap_id(object) do
|
||||||
|
@ -95,6 +97,21 @@ def generate_id(type) do
|
||||||
"#{Web.base_url()}/#{type}/#{UUID.generate()}"
|
"#{Web.base_url()}/#{type}/#{UUID.generate()}"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def get_notified_from_object(%{"type" => type} = object) when type in @supported_object_types do
|
||||||
|
fake_create_activity = %{
|
||||||
|
"to" => object["to"],
|
||||||
|
"cc" => object["cc"],
|
||||||
|
"type" => "Create",
|
||||||
|
"object" => object
|
||||||
|
}
|
||||||
|
|
||||||
|
Notification.get_notified_from_activity(%Activity{data: fake_create_activity}, false)
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_notified_from_object(object) do
|
||||||
|
Notification.get_notified_from_activity(%Activity{data: object}, false)
|
||||||
|
end
|
||||||
|
|
||||||
def create_context(context) do
|
def create_context(context) do
|
||||||
context = context || generate_id("contexts")
|
context = context || generate_id("contexts")
|
||||||
changeset = Object.context_mapping(context)
|
changeset = Object.context_mapping(context)
|
||||||
|
@ -164,7 +181,7 @@ def lazy_put_object_defaults(map, activity \\ %{}) do
|
||||||
Inserts a full object if it is contained in an activity.
|
Inserts a full object if it is contained in an activity.
|
||||||
"""
|
"""
|
||||||
def insert_full_object(%{"object" => %{"type" => type} = object_data})
|
def insert_full_object(%{"object" => %{"type" => type} = object_data})
|
||||||
when is_map(object_data) and type in ["Article", "Note", "Video", "Page"] do
|
when is_map(object_data) and type in @supported_object_types do
|
||||||
with {:ok, _} <- Object.create(object_data) do
|
with {:ok, _} <- Object.create(object_data) do
|
||||||
:ok
|
:ok
|
||||||
end
|
end
|
||||||
|
|
|
@ -34,21 +34,29 @@ def attachments_from_ids(ids) do
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_for_user_and_mentions(user, mentions, inReplyTo, "public") do
|
def to_for_user_and_mentions(user, mentions, inReplyTo, "public") do
|
||||||
to = ["https://www.w3.org/ns/activitystreams#Public"]
|
|
||||||
|
|
||||||
mentioned_users = Enum.map(mentions, fn {_, %{ap_id: ap_id}} -> ap_id end)
|
mentioned_users = Enum.map(mentions, fn {_, %{ap_id: ap_id}} -> ap_id end)
|
||||||
cc = [user.follower_address | mentioned_users]
|
|
||||||
|
to = ["https://www.w3.org/ns/activitystreams#Public" | mentioned_users]
|
||||||
|
cc = [user.follower_address]
|
||||||
|
|
||||||
if inReplyTo do
|
if inReplyTo do
|
||||||
{to, Enum.uniq([inReplyTo.data["actor"] | cc])}
|
{Enum.uniq([inReplyTo.data["actor"] | to]), cc}
|
||||||
else
|
else
|
||||||
{to, cc}
|
{to, cc}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_for_user_and_mentions(user, mentions, inReplyTo, "unlisted") do
|
def to_for_user_and_mentions(user, mentions, inReplyTo, "unlisted") do
|
||||||
{to, cc} = to_for_user_and_mentions(user, mentions, inReplyTo, "public")
|
mentioned_users = Enum.map(mentions, fn {_, %{ap_id: ap_id}} -> ap_id end)
|
||||||
{cc, to}
|
|
||||||
|
to = [user.follower_address | mentioned_users]
|
||||||
|
cc = ["https://www.w3.org/ns/activitystreams#Public"]
|
||||||
|
|
||||||
|
if inReplyTo do
|
||||||
|
{Enum.uniq([inReplyTo.data["actor"] | to]), cc}
|
||||||
|
else
|
||||||
|
{to, cc}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_for_user_and_mentions(user, mentions, inReplyTo, "private") do
|
def to_for_user_and_mentions(user, mentions, inReplyTo, "private") do
|
||||||
|
|
|
@ -3,6 +3,7 @@ defmodule Pleroma.NotificationTest do
|
||||||
alias Pleroma.Web.TwitterAPI.TwitterAPI
|
alias Pleroma.Web.TwitterAPI.TwitterAPI
|
||||||
alias Pleroma.Web.CommonAPI
|
alias Pleroma.Web.CommonAPI
|
||||||
alias Pleroma.{User, Notification}
|
alias Pleroma.{User, Notification}
|
||||||
|
alias Pleroma.Web.ActivityPub.Transmogrifier
|
||||||
import Pleroma.Factory
|
import Pleroma.Factory
|
||||||
|
|
||||||
describe "create_notifications" do
|
describe "create_notifications" do
|
||||||
|
@ -156,6 +157,100 @@ test "it sets all notifications as read up to a specified notification ID" do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "notification target determination" do
|
||||||
|
test "it sends notifications to addressed users in new messages" do
|
||||||
|
user = insert(:user)
|
||||||
|
other_user = insert(:user)
|
||||||
|
|
||||||
|
{:ok, activity} =
|
||||||
|
CommonAPI.post(user, %{
|
||||||
|
"status" => "hey @#{other_user.nickname}!"
|
||||||
|
})
|
||||||
|
|
||||||
|
assert other_user in Notification.get_notified_from_activity(activity)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it sends notifications to mentioned users in new messages" do
|
||||||
|
user = insert(:user)
|
||||||
|
other_user = insert(:user)
|
||||||
|
|
||||||
|
create_activity = %{
|
||||||
|
"@context" => "https://www.w3.org/ns/activitystreams",
|
||||||
|
"type" => "Create",
|
||||||
|
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
|
||||||
|
"actor" => user.ap_id,
|
||||||
|
"object" => %{
|
||||||
|
"type" => "Note",
|
||||||
|
"content" => "message with a Mention tag, but no explicit tagging",
|
||||||
|
"tag" => [
|
||||||
|
%{
|
||||||
|
"type" => "Mention",
|
||||||
|
"href" => other_user.ap_id,
|
||||||
|
"name" => other_user.nickname
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"attributedTo" => user.ap_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{:ok, activity} = Transmogrifier.handle_incoming(create_activity)
|
||||||
|
|
||||||
|
assert other_user in Notification.get_notified_from_activity(activity)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it does not send notifications to users who are only cc in new messages" do
|
||||||
|
user = insert(:user)
|
||||||
|
other_user = insert(:user)
|
||||||
|
|
||||||
|
create_activity = %{
|
||||||
|
"@context" => "https://www.w3.org/ns/activitystreams",
|
||||||
|
"type" => "Create",
|
||||||
|
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
|
||||||
|
"cc" => [other_user.ap_id],
|
||||||
|
"actor" => user.ap_id,
|
||||||
|
"object" => %{
|
||||||
|
"type" => "Note",
|
||||||
|
"content" => "hi everyone",
|
||||||
|
"attributedTo" => user.ap_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{:ok, activity} = Transmogrifier.handle_incoming(create_activity)
|
||||||
|
|
||||||
|
assert other_user not in Notification.get_notified_from_activity(activity)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it does not send notification to mentioned users in likes" do
|
||||||
|
user = insert(:user)
|
||||||
|
other_user = insert(:user)
|
||||||
|
third_user = insert(:user)
|
||||||
|
|
||||||
|
{:ok, activity_one} =
|
||||||
|
CommonAPI.post(user, %{
|
||||||
|
"status" => "hey @#{other_user.nickname}!"
|
||||||
|
})
|
||||||
|
|
||||||
|
{:ok, activity_two, _} = CommonAPI.favorite(activity_one.id, third_user)
|
||||||
|
|
||||||
|
assert other_user not in Notification.get_notified_from_activity(activity_two)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it does not send notification to mentioned users in announces" do
|
||||||
|
user = insert(:user)
|
||||||
|
other_user = insert(:user)
|
||||||
|
third_user = insert(:user)
|
||||||
|
|
||||||
|
{:ok, activity_one} =
|
||||||
|
CommonAPI.post(user, %{
|
||||||
|
"status" => "hey @#{other_user.nickname}!"
|
||||||
|
})
|
||||||
|
|
||||||
|
{:ok, activity_two, _} = CommonAPI.repeat(activity_one.id, third_user)
|
||||||
|
|
||||||
|
assert other_user not in Notification.get_notified_from_activity(activity_two)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "notification lifecycle" do
|
describe "notification lifecycle" do
|
||||||
test "liking an activity results in 1 notification, then 0 if the activity is deleted" do
|
test "liking an activity results in 1 notification, then 0 if the activity is deleted" do
|
||||||
user = insert(:user)
|
user = insert(:user)
|
||||||
|
|
|
@ -48,7 +48,7 @@ test "create a status" do
|
||||||
"https://www.w3.org/ns/activitystreams#Public"
|
"https://www.w3.org/ns/activitystreams#Public"
|
||||||
)
|
)
|
||||||
|
|
||||||
assert Enum.member?(get_in(activity.data, ["cc"]), "shp")
|
assert Enum.member?(get_in(activity.data, ["to"]), "shp")
|
||||||
assert activity.local == true
|
assert activity.local == true
|
||||||
|
|
||||||
assert %{"moominmamma" => "http://localhost:4001/finmoji/128px/moominmamma-128.png"} =
|
assert %{"moominmamma" => "http://localhost:4001/finmoji/128px/moominmamma-128.png"} =
|
||||||
|
|
Loading…
Reference in a new issue