diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 8f1839c426..09ce2efd90 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -169,25 +169,6 @@ unit-testing-1.12-erratic:
- mix ecto.migrate
- mix test --only=erratic
-unit-testing-1.12-rum:
- extends:
- - .build_changes_policy
- - .using-ci-base
- stage: test
- cache: *testing_cache_policy
- services:
- - name: git.pleroma.social:5050/pleroma/pleroma/postgres-with-rum-13
- alias: postgres
- command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
- variables:
- <<: *global_variables
- RUM_ENABLED: "true"
- script:
- - mix ecto.create
- - mix ecto.migrate
- - "mix ecto.migrate --migrations-path priv/repo/optional_migrations/rum_indexing/"
- - mix test --preload-modules
-
formatting-1.13:
extends: .build_changes_policy
image: &formatting_elixir elixir:1.13-alpine
diff --git a/changelog.d/force-mention-mrf.add b/changelog.d/force-mention-mrf.add
new file mode 100644
index 0000000000..46ac14244b
--- /dev/null
+++ b/changelog.d/force-mention-mrf.add
@@ -0,0 +1 @@
+Add ForceMention MRF
\ No newline at end of file
diff --git a/changelog.d/issue-3241.fix b/changelog.d/issue-3241.fix
new file mode 100644
index 0000000000..d46db9805a
--- /dev/null
+++ b/changelog.d/issue-3241.fix
@@ -0,0 +1 @@
+Handle cases when users.inbox is nil.
diff --git a/changelog.d/notifications.fix b/changelog.d/notifications.fix
new file mode 100644
index 0000000000..a2d2eaea90
--- /dev/null
+++ b/changelog.d/notifications.fix
@@ -0,0 +1 @@
+Notifications: improve performance by filtering on users table instead of activities table
\ No newline at end of file
diff --git a/changelog.d/public-polls.add b/changelog.d/public-polls.add
new file mode 100644
index 0000000000..0dae0c38ec
--- /dev/null
+++ b/changelog.d/public-polls.add
@@ -0,0 +1 @@
+Expose nonAnonymous field from Smithereen polls
\ No newline at end of file
diff --git a/config/config.exs b/config/config.exs
index 5d3f4eebf5..6adf3386b0 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -436,6 +436,10 @@
ttl: 60_000,
min_length: 50
+config :pleroma, :mrf_force_mention,
+ mention_parent: true,
+ mention_quoted: true
+
config :pleroma, :rich_media,
enabled: true,
ignore_hosts: [],
diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md
index f54a87045a..4aeae4f2c0 100644
--- a/docs/configuration/cheatsheet.md
+++ b/docs/configuration/cheatsheet.md
@@ -157,7 +157,8 @@ To add configuration to your config file, you can copy it from the base config.
* `Pleroma.Web.ActivityPub.MRF.KeywordPolicy`: Rejects or removes from the federated timeline or replaces keywords. (See [`:mrf_keyword`](#mrf_keyword)).
* `Pleroma.Web.ActivityPub.MRF.ForceMentionsInContent`: Forces every mentioned user to be reflected in the post content.
* `Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy`: Forces quote post URLs to be reflected in the message content inline.
- * `Pleroma.Web.ActivityPub.MRF.QuoteToLinkTagPolicy`: Force a Link tag for posts quoting another post. (may break outgoing federation of quote posts with older Pleroma versions)
+ * `Pleroma.Web.ActivityPub.MRF.QuoteToLinkTagPolicy`: Force a Link tag for posts quoting another post. (may break outgoing federation of quote posts with older Pleroma versions).
+ * `Pleroma.Web.ActivityPub.MRF.ForceMention`: Forces posts to include a mention of the author of parent post or the author of quoted post.
* `transparency`: Make the content of your Message Rewrite Facility settings public (via nodeinfo).
* `transparency_exclusions`: Exclude specific instance names from MRF transparency. The use of the exclusions feature will be disclosed in nodeinfo as a boolean value.
@@ -271,6 +272,10 @@ Notes:
#### :mrf_inline_quote
* `template`: The template to append to the post. `{url}` will be replaced with the actual link to the quoted post. Default: `RT: {url}`
+#### :mrf_force_mention
+* `mention_parent`: Whether to append mention of parent post author
+* `mention_quoted`: Whether to append mention of parent quoted author
+
### :activitypub
* `unfollow_blocked`: Whether blocks result in people getting unfollowed
* `outgoing_blocks`: Whether to federate blocks to other instances
diff --git a/docs/development/API/differences_in_mastoapi_responses.md b/docs/development/API/differences_in_mastoapi_responses.md
index abf03c9838..c9038822c2 100644
--- a/docs/development/API/differences_in_mastoapi_responses.md
+++ b/docs/development/API/differences_in_mastoapi_responses.md
@@ -41,6 +41,7 @@ Has these additional fields under the `pleroma` object:
- `pinned_at`: a datetime (iso8601) when status was pinned, `null` otherwise.
- `quotes_count`: the count of status quotes.
- `event`: event information if the post is an event, `null` otherwise.
+- `non_anonymous`: true if the source post specifies the poll results are not anonymous. Currently only implemented by Smithereen.
- `bookmark_folder`: the ID of the folder bookmark is stored within (if any).
The `GET /api/v1/statuses/:id/source` endpoint additionally has the following attributes:
diff --git a/lib/pleroma/following_relationship.ex b/lib/pleroma/following_relationship.ex
index 15664c876c..f38c2fce9c 100644
--- a/lib/pleroma/following_relationship.ex
+++ b/lib/pleroma/following_relationship.ex
@@ -241,13 +241,13 @@ def find(following_relationships, follower, following) do
end
@doc """
- For a query with joined activity,
- keeps rows where activity's actor is followed by user -or- is NOT domain-blocked by user.
+ For a query with joined activity's actor,
+ keeps rows where actor is followed by user -or- is NOT domain-blocked by user.
"""
def keep_following_or_not_domain_blocked(query, user) do
where(
query,
- [_, activity],
+ [_, user_actor: user_actor],
fragment(
# "(actor's domain NOT in domain_blocks) OR (actor IS in followed AP IDs)"
"""
@@ -255,9 +255,9 @@ def keep_following_or_not_domain_blocked(query, user) do
? = ANY(SELECT ap_id FROM users AS u INNER JOIN following_relationships AS fr
ON u.id = fr.following_id WHERE fr.follower_id = ? AND fr.state = ?)
""",
- activity.actor,
+ user_actor.ap_id,
^user.domain_blocks,
- activity.actor,
+ user_actor.ap_id,
^User.binary_id(user.id),
^accept_state_code()
)
diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex
index 2833570979..288558db79 100644
--- a/lib/pleroma/notification.ex
+++ b/lib/pleroma/notification.ex
@@ -142,7 +142,7 @@ defp exclude_blocked(query, user, opts) do
blocked_ap_ids = opts[:blocked_users_ap_ids] || User.blocked_users_ap_ids(user)
query
- |> where([n, a], a.actor not in ^blocked_ap_ids)
+ |> where([..., user_actor: user_actor], user_actor.ap_id not in ^blocked_ap_ids)
|> FollowingRelationship.keep_following_or_not_domain_blocked(user)
end
@@ -153,7 +153,7 @@ defp exclude_blockers(query, user) do
blocker_ap_ids = User.incoming_relationships_ungrouped_ap_ids(user, [:block])
query
- |> where([n, a], a.actor not in ^blocker_ap_ids)
+ |> where([..., user_actor: user_actor], user_actor.ap_id not in ^blocker_ap_ids)
end
end
@@ -166,7 +166,7 @@ defp exclude_notification_muted(query, user, opts) do
opts[:notification_muted_users_ap_ids] || User.notification_muted_users_ap_ids(user)
query
- |> where([n, a], a.actor not in ^notification_muted_ap_ids)
+ |> where([..., user_actor: user_actor], user_actor.ap_id not in ^notification_muted_ap_ids)
|> join(:left, [n, a], tm in ThreadMute,
on: tm.user_id == ^user.id and tm.context == fragment("?->>'context'", a.data),
as: :thread_mute
diff --git a/lib/pleroma/web/activity_pub/mrf/force_mention.ex b/lib/pleroma/web/activity_pub/mrf/force_mention.ex
new file mode 100644
index 0000000000..3853489fcf
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/mrf/force_mention.ex
@@ -0,0 +1,59 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2024 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.MRF.ForceMention do
+ require Pleroma.Constants
+
+ alias Pleroma.Config
+ alias Pleroma.Object
+ alias Pleroma.User
+
+ @behaviour Pleroma.Web.ActivityPub.MRF.Policy
+
+ defp get_author(url) do
+ with %Object{data: %{"actor" => actor}} <- Object.normalize(url, fetch: false),
+ %User{ap_id: ap_id, nickname: nickname} <- User.get_cached_by_ap_id(actor) do
+ %{"type" => "Mention", "href" => ap_id, "name" => "@#{nickname}"}
+ else
+ _ -> nil
+ end
+ end
+
+ defp prepend_author(tags, _, false), do: tags
+
+ defp prepend_author(tags, nil, _), do: tags
+
+ defp prepend_author(tags, url, _) do
+ actor = get_author(url)
+
+ if not is_nil(actor) do
+ [actor | tags]
+ else
+ tags
+ end
+ end
+
+ @impl true
+ def filter(%{"type" => "Create", "object" => %{"tag" => tag} = object} = activity) do
+ tag =
+ tag
+ |> prepend_author(
+ object["inReplyTo"],
+ Config.get([:mrf_force_mention, :mention_parent, true])
+ )
+ |> prepend_author(
+ object["quoteUrl"],
+ Config.get([:mrf_force_mention, :mention_quoted, true])
+ )
+ |> Enum.uniq()
+
+ {:ok, put_in(activity["object"]["tag"], tag)}
+ end
+
+ @impl true
+ def filter(object), do: {:ok, object}
+
+ @impl true
+ def describe, do: {:ok, %{}}
+end
diff --git a/lib/pleroma/web/activity_pub/publisher.ex b/lib/pleroma/web/activity_pub/publisher.ex
index 9e7d005192..a42b4844e8 100644
--- a/lib/pleroma/web/activity_pub/publisher.ex
+++ b/lib/pleroma/web/activity_pub/publisher.ex
@@ -158,19 +158,18 @@ defp signature_host(%URI{port: port, scheme: scheme, host: host}) do
end
end
- defp should_federate?(inbox, public) do
- if public do
- true
- else
- %{host: host} = URI.parse(inbox)
+ def should_federate?(nil, _), do: false
+ def should_federate?(_, true), do: true
- quarantined_instances =
- Config.get([:instance, :quarantined_instances], [])
- |> Pleroma.Web.ActivityPub.MRF.instance_list_from_tuples()
- |> Pleroma.Web.ActivityPub.MRF.subdomains_regex()
+ def should_federate?(inbox, _) do
+ %{host: host} = URI.parse(inbox)
- !Pleroma.Web.ActivityPub.MRF.subdomain_match?(quarantined_instances, host)
- end
+ quarantined_instances =
+ Config.get([:instance, :quarantined_instances], [])
+ |> Pleroma.Web.ActivityPub.MRF.instance_list_from_tuples()
+ |> Pleroma.Web.ActivityPub.MRF.subdomains_regex()
+
+ !Pleroma.Web.ActivityPub.MRF.subdomain_match?(quarantined_instances, host)
end
@spec recipients(User.t(), Activity.t()) :: [[User.t()]]
diff --git a/lib/pleroma/web/api_spec/schemas/poll.ex b/lib/pleroma/web/api_spec/schemas/poll.ex
index cb2ffdc68a..20cf5b061b 100644
--- a/lib/pleroma/web/api_spec/schemas/poll.ex
+++ b/lib/pleroma/web/api_spec/schemas/poll.ex
@@ -60,7 +60,10 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Poll do
pleroma: %Schema{
type: :object,
properties: %{
- non_anonymous: %Schema{type: :boolean, description: "Is the voters collection public?"}
+ non_anonymous: %Schema{
+ type: :boolean,
+ description: "Can voters be publicly identified?"
+ }
}
}
},
diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex
index 03c68b2753..5fd732bca0 100644
--- a/lib/pleroma/web/mastodon_api/views/instance_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex
@@ -174,6 +174,7 @@ def features do
end,
"pleroma:get:main/ostatus",
"pleroma:group_actors",
+ "pleroma:bookmark_folders",
if Pleroma.Language.Translation.configured?() do
"translation"
end,
diff --git a/priv/static/schemas/litepub-0.1.jsonld b/priv/static/schemas/litepub-0.1.jsonld
index df377cd189..572b690302 100644
--- a/priv/static/schemas/litepub-0.1.jsonld
+++ b/priv/static/schemas/litepub-0.1.jsonld
@@ -42,6 +42,7 @@
"vcard": "http://www.w3.org/2006/vcard/ns#",
"formerRepresentations": "litepub:formerRepresentations",
"sm": "http://smithereen.software/ns#",
+<<<<<<< HEAD
"nonAnonymous": "sm:nonAnonymous",
"votersCount": "toot:votersCount",
"mz": "https://joinmobilizon.org/ns#",
@@ -66,6 +67,9 @@
"@id": "schema:location",
"@type": "schema:Place"
}
+=======
+ "nonAnonymous": "sm:nonAnonymous"
+>>>>>>> origin/develop
}
]
}
diff --git a/test/fixtures/minds-invalid-mention-post.json b/test/fixtures/minds-invalid-mention-post.json
new file mode 100644
index 0000000000..ea2cb27390
--- /dev/null
+++ b/test/fixtures/minds-invalid-mention-post.json
@@ -0,0 +1 @@
+{"@context":"https://www.w3.org/ns/activitystreams","type":"Note","id":"https://www.minds.com/api/activitypub/users/1198929502760083472/entities/urn:comment:1600926863310458883:0:0:0:1600932467852709903","attributedTo":"https://www.minds.com/api/activitypub/users/1198929502760083472","content":"\u003Ca class=\u0022u-url mention\u0022 href=\u0022https://www.minds.com/lain\u0022 target=\u0022_blank\u0022\u003E@lain\u003C/a\u003E corn syrup.","to":["https://www.w3.org/ns/activitystreams#Public"],"cc":["https://www.minds.com/api/activitypub/users/1198929502760083472/followers","https://lain.com/users/lain"],"tag":[{"type":"Mention","href":"https://www.minds.com/api/activitypub/users/464237775479123984","name":"@lain"}],"url":"https://www.minds.com/newsfeed/1600926863310458883?focusedCommentUrn=urn:comment:1600926863310458883:0:0:0:1600932467852709903","published":"2024-02-04T17:34:03+00:00","inReplyTo":"https://lain.com/objects/36254095-c839-4167-bcc2-b361d5de9198","source":{"content":"@lain corn syrup.","mediaType":"text/plain"}}
\ No newline at end of file
diff --git a/test/fixtures/minds-pleroma-mentioned-post.json b/test/fixtures/minds-pleroma-mentioned-post.json
new file mode 100644
index 0000000000..9dfa42c909
--- /dev/null
+++ b/test/fixtures/minds-pleroma-mentioned-post.json
@@ -0,0 +1 @@
+{"@context":["https://www.w3.org/ns/activitystreams","https://lain.com/schemas/litepub-0.1.jsonld",{"@language":"und"}],"actor":"https://lain.com/users/lain","attachment":[],"attributedTo":"https://lain.com/users/lain","cc":["https://lain.com/users/lain/followers"],"content":"which diet is the best for cognitive dissonance","context":"https://lain.com/contexts/98c8a130-e813-4797-8973-600e80114317","conversation":"https://lain.com/contexts/98c8a130-e813-4797-8973-600e80114317","id":"https://lain.com/objects/36254095-c839-4167-bcc2-b361d5de9198","published":"2024-02-04T17:11:23.931890Z","repliesCount":11,"sensitive":null,"source":{"content":"which diet is the best for cognitive dissonance","mediaType":"text/plain"},"summary":"","tag":[],"to":["https://www.w3.org/ns/activitystreams#Public"],"type":"Note"}
\ No newline at end of file
diff --git a/test/pleroma/web/activity_pub/mrf/force_mention_test.exs b/test/pleroma/web/activity_pub/mrf/force_mention_test.exs
new file mode 100644
index 0000000000..b026bab660
--- /dev/null
+++ b/test/pleroma/web/activity_pub/mrf/force_mention_test.exs
@@ -0,0 +1,73 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2024 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.MRF.ForceMentionTest do
+ use Pleroma.DataCase
+ require Pleroma.Constants
+
+ alias Pleroma.Web.ActivityPub.MRF.ForceMention
+
+ import Pleroma.Factory
+
+ test "adds mention to a reply" do
+ lain =
+ insert(:user, ap_id: "https://lain.com/users/lain", nickname: "lain@lain.com", local: false)
+
+ niobleoum =
+ insert(:user,
+ ap_id: "https://www.minds.com/api/activitypub/users/1198929502760083472",
+ nickname: "niobleoum@minds.com",
+ local: false
+ )
+
+ status = File.read!("test/fixtures/minds-pleroma-mentioned-post.json") |> Jason.decode!()
+
+ status_activity = %{
+ "type" => "Create",
+ "actor" => lain.ap_id,
+ "object" => status
+ }
+
+ Pleroma.Web.ActivityPub.Transmogrifier.handle_incoming(status_activity)
+
+ reply = File.read!("test/fixtures/minds-invalid-mention-post.json") |> Jason.decode!()
+
+ reply_activity = %{
+ "type" => "Create",
+ "actor" => niobleoum.ap_id,
+ "object" => reply
+ }
+
+ {:ok, %{"object" => %{"tag" => tag}}} = ForceMention.filter(reply_activity)
+
+ assert Enum.find(tag, fn %{"href" => href} -> href == lain.ap_id end)
+ end
+
+ test "adds mention to a quote" do
+ user1 = insert(:user, ap_id: "https://misskey.io/users/83ssedkv53")
+ user2 = insert(:user, ap_id: "https://misskey.io/users/7rkrarq81i")
+
+ status = File.read!("test/fixtures/tesla_mock/misskey.io_8vs6wxufd0.json") |> Jason.decode!()
+
+ status_activity = %{
+ "type" => "Create",
+ "actor" => user1.ap_id,
+ "object" => status
+ }
+
+ Pleroma.Web.ActivityPub.Transmogrifier.handle_incoming(status_activity)
+
+ quote_post = File.read!("test/fixtures/quote_post/misskey_quote_post.json") |> Jason.decode!()
+
+ quote_activity = %{
+ "type" => "Create",
+ "actor" => user2.ap_id,
+ "object" => quote_post
+ }
+
+ {:ok, %{"object" => %{"tag" => tag}}} = ForceMention.filter(quote_activity)
+
+ assert Enum.find(tag, fn %{"href" => href} -> href == user1.ap_id end)
+ end
+end
diff --git a/test/pleroma/web/activity_pub/publisher_test.exs b/test/pleroma/web/activity_pub/publisher_test.exs
index 7aa06a5c44..870f1f77a7 100644
--- a/test/pleroma/web/activity_pub/publisher_test.exs
+++ b/test/pleroma/web/activity_pub/publisher_test.exs
@@ -25,6 +25,17 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do
setup_all do: clear_config([:instance, :federating], true)
+ describe "should_federate?/1" do
+ test "it returns false when the inbox is nil" do
+ refute Publisher.should_federate?(nil, false)
+ refute Publisher.should_federate?(nil, true)
+ end
+
+ test "it returns true when public is true" do
+ assert Publisher.should_federate?(false, true)
+ end
+ end
+
describe "gather_webfinger_links/1" do
test "it returns links" do
user = insert(:user)
@@ -205,6 +216,7 @@ test "publish to url with with different ports" do
refute called(Instances.set_reachable(inbox))
end
+ @tag capture_log: true
test_with_mock "calls `Instances.set_unreachable` on target inbox on non-2xx HTTP response code",
Instances,
[:passthrough],
diff --git a/test/pleroma/web/mastodon_api/views/poll_view_test.exs b/test/pleroma/web/mastodon_api/views/poll_view_test.exs
index 4c0e2ed41a..e3508d0757 100644
--- a/test/pleroma/web/mastodon_api/views/poll_view_test.exs
+++ b/test/pleroma/web/mastodon_api/views/poll_view_test.exs
@@ -181,7 +181,7 @@ test "displays correct voters count basing on voters array" do
assert result[:voters_count] == 4
end
- test "detects that poll is non anonymous" do
+ test "that poll is non anonymous" do
object = Object.normalize("https://friends.grishka.me/posts/54642", fetch: true)
result = PollView.render("show.json", %{object: object})
diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex
index c2f41c63f7..50216470ea 100644
--- a/test/support/http_request_mock.ex
+++ b/test/support/http_request_mock.ex
@@ -1675,6 +1675,24 @@ def get("https://example.com/empty", _, _, _) do
{:ok, %Tesla.Env{status: 200, body: "hello"}}
end
+ def get("https://friends.grishka.me/posts/54642", _, _, _) do
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/tesla_mock/smithereen_non_anonymous_poll.json"),
+ headers: activitypub_object_headers()
+ }}
+ end
+
+ def get("https://friends.grishka.me/users/1", _, _, _) do
+ {:ok,
+ %Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/tesla_mock/smithereen_user.json"),
+ headers: activitypub_object_headers()
+ }}
+ end
+
def get(url, query, body, headers) do
{:error,
"Mock response not implemented for GET #{inspect(url)}, #{query}, #{inspect(body)}, #{inspect(headers)}"}