From 62e42b03abd2cede85e85f62c35f62a8c42e8ea1 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Wed, 15 May 2019 20:10:16 +0300 Subject: [PATCH 01/52] Handle incoming Question objects --- .../web/activity_pub/transmogrifier.ex | 2 +- lib/pleroma/web/activity_pub/utils.ex | 2 +- test/fixtures/httpoison_mock/rinpatch.json | 64 ++++++++++++ test/fixtures/mastodon-question-activity.json | 99 +++++++++++++++++++ test/support/http_request_mock.ex | 8 ++ test/web/activity_pub/transmogrifier_test.exs | 17 ++++ 6 files changed, 190 insertions(+), 2 deletions(-) create mode 100644 test/fixtures/httpoison_mock/rinpatch.json create mode 100644 test/fixtures/mastodon-question-activity.json diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 508f3532f..c2596cfec 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -399,7 +399,7 @@ def handle_incoming(%{"id" => id}) when not (is_binary(id) and length(id) > 8), # - tags # - emoji def handle_incoming(%{"type" => "Create", "object" => %{"type" => objtype} = object} = data) - when objtype in ["Article", "Note", "Video", "Page"] do + when objtype in ["Article", "Note", "Video", "Page", "Question"] do actor = Containment.get_actor(data) data = diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index 581b9d1ab..de91fa03f 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -19,7 +19,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do require Logger - @supported_object_types ["Article", "Note", "Video", "Page"] + @supported_object_types ["Article", "Note", "Video", "Page", "Question"] # 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. diff --git a/test/fixtures/httpoison_mock/rinpatch.json b/test/fixtures/httpoison_mock/rinpatch.json new file mode 100644 index 000000000..59311ecb6 --- /dev/null +++ b/test/fixtures/httpoison_mock/rinpatch.json @@ -0,0 +1,64 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "toot": "http://joinmastodon.org/ns#", + "featured": { + "@id": "toot:featured", + "@type": "@id" + }, + "alsoKnownAs": { + "@id": "as:alsoKnownAs", + "@type": "@id" + }, + "movedTo": { + "@id": "as:movedTo", + "@type": "@id" + }, + "schema": "http://schema.org#", + "PropertyValue": "schema:PropertyValue", + "value": "schema:value", + "Hashtag": "as:Hashtag", + "Emoji": "toot:Emoji", + "IdentityProof": "toot:IdentityProof", + "focalPoint": { + "@container": "@list", + "@id": "toot:focalPoint" + } + } + ], + "id": "https://mastodon.sdf.org/users/rinpatch", + "type": "Person", + "following": "https://mastodon.sdf.org/users/rinpatch/following", + "followers": "https://mastodon.sdf.org/users/rinpatch/followers", + "inbox": "https://mastodon.sdf.org/users/rinpatch/inbox", + "outbox": "https://mastodon.sdf.org/users/rinpatch/outbox", + "featured": "https://mastodon.sdf.org/users/rinpatch/collections/featured", + "preferredUsername": "rinpatch", + "name": "rinpatch", + "summary": "

umu

", + "url": "https://mastodon.sdf.org/@rinpatch", + "manuallyApprovesFollowers": false, + "publicKey": { + "id": "https://mastodon.sdf.org/users/rinpatch#main-key", + "owner": "https://mastodon.sdf.org/users/rinpatch", + "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1vbhYKDopb5xzfJB2TZY\n0ZvgxqdAhbSKKkQC5Q2b0ofhvueDy2AuZTnVk1/BbHNlqKlwhJUSpA6LiTZVvtcc\nMn6cmSaJJEg30gRF5GARP8FMcuq8e2jmceiW99NnUX17MQXsddSf2JFUwD0rUE8H\nBsgD7UzE9+zlA/PJOTBO7fvBEz9PTQ3r4sRMTJVFvKz2MU/U+aRNTuexRKMMPnUw\nfp6VWh1F44VWJEQOs4tOEjGiQiMQh5OfBk1w2haT3vrDbQvq23tNpUP1cRomLUtx\nEBcGKi5DMMBzE1RTVT1YUykR/zLWlA+JSmw7P6cWtsHYZovs8dgn8Po3X//6N+ng\nTQIDAQAB\n-----END PUBLIC KEY-----\n" + }, + "tag": [], + "attachment": [], + "endpoints": { + "sharedInbox": "https://mastodon.sdf.org/inbox" + }, + "icon": { + "type": "Image", + "mediaType": "image/jpeg", + "url": "https://mastodon.sdf.org/system/accounts/avatars/000/067/580/original/bf05521bf711b7a0.jpg?1533238802" + }, + "image": { + "type": "Image", + "mediaType": "image/gif", + "url": "https://mastodon.sdf.org/system/accounts/headers/000/067/580/original/a99b987e798f7063.gif?1533278217" + } +} diff --git a/test/fixtures/mastodon-question-activity.json b/test/fixtures/mastodon-question-activity.json new file mode 100644 index 000000000..ac329c7d5 --- /dev/null +++ b/test/fixtures/mastodon-question-activity.json @@ -0,0 +1,99 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + { + "ostatus": "http://ostatus.org#", + "atomUri": "ostatus:atomUri", + "inReplyToAtomUri": "ostatus:inReplyToAtomUri", + "conversation": "ostatus:conversation", + "sensitive": "as:sensitive", + "Hashtag": "as:Hashtag", + "toot": "http://joinmastodon.org/ns#", + "Emoji": "toot:Emoji", + "focalPoint": { + "@container": "@list", + "@id": "toot:focalPoint" + } + } + ], + "id": "https://mastodon.sdf.org/users/rinpatch/statuses/102070944809637304/activity", + "type": "Create", + "actor": "https://mastodon.sdf.org/users/rinpatch", + "published": "2019-05-10T09:03:36Z", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [ + "https://mastodon.sdf.org/users/rinpatch/followers" + ], + "object": { + "id": "https://mastodon.sdf.org/users/rinpatch/statuses/102070944809637304", + "type": "Question", + "summary": null, + "inReplyTo": null, + "published": "2019-05-10T09:03:36Z", + "url": "https://mastodon.sdf.org/@rinpatch/102070944809637304", + "attributedTo": "https://mastodon.sdf.org/users/rinpatch", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [ + "https://mastodon.sdf.org/users/rinpatch/followers" + ], + "sensitive": false, + "atomUri": "https://mastodon.sdf.org/users/rinpatch/statuses/102070944809637304", + "inReplyToAtomUri": null, + "conversation": "tag:mastodon.sdf.org,2019-05-10:objectId=15095122:objectType=Conversation", + "content": "

Why is Tenshi eating a corndog so cute?

", + "contentMap": { + "en": "

Why is Tenshi eating a corndog so cute?

" + }, + "endTime": "2019-05-11T09:03:36Z", + "closed": "2019-05-11T09:03:36Z", + "attachment": [], + "tag": [], + "replies": { + "id": "https://mastodon.sdf.org/users/rinpatch/statuses/102070944809637304/replies", + "type": "Collection", + "first": { + "type": "CollectionPage", + "partOf": "https://mastodon.sdf.org/users/rinpatch/statuses/102070944809637304/replies", + "items": [] + } + }, + "oneOf": [ + { + "type": "Note", + "name": "Dunno", + "replies": { + "type": "Collection", + "totalItems": 0 + } + }, + { + "type": "Note", + "name": "Everyone knows that!", + "replies": { + "type": "Collection", + "totalItems": 1 + } + }, + { + "type": "Note", + "name": "25 char limit is dumb", + "replies": { + "type": "Collection", + "totalItems": 0 + } + }, + { + "type": "Note", + "name": "I can't even fit a funny", + "replies": { + "type": "Collection", + "totalItems": 1 + } + } + ] + } +} diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex index 5b355bfe6..3064c032b 100644 --- a/test/support/http_request_mock.ex +++ b/test/support/http_request_mock.ex @@ -52,6 +52,14 @@ def get("https://mastodon.social/users/emelie", _, _, _) do }} end + def get("https://mastodon.sdf.org/users/rinpatch", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/httpoison_mock/rinpatch.json") + }} + end + def get( "https://mastodon.social/.well-known/webfinger?resource=https://mastodon.social/users/emelie", _, diff --git a/test/web/activity_pub/transmogrifier_test.exs b/test/web/activity_pub/transmogrifier_test.exs index c24b50f8c..727abbd17 100644 --- a/test/web/activity_pub/transmogrifier_test.exs +++ b/test/web/activity_pub/transmogrifier_test.exs @@ -113,6 +113,23 @@ test "it works for incoming notices with hashtags" do assert Enum.at(object.data["tag"], 2) == "moo" end + test "it works for incoming questions" do + data = File.read!("test/fixtures/mastodon-question-activity.json") |> Poison.decode!() + + {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) + + object = Object.normalize(activity) + + assert Enum.all?(object.data["oneOf"], fn choice -> + choice["name"] in [ + "Dunno", + "Everyone knows that!", + "25 char limit is dumb", + "I can't even fit a funny" + ] + end) + end + test "it works for incoming notices with contentMap" do data = File.read!("test/fixtures/mastodon-post-activity-contentmap.json") |> Poison.decode!() From 642a67dd4492f31b5b9fe457e34c1589c9d70c3f Mon Sep 17 00:00:00 2001 From: rinpatch Date: Fri, 17 May 2019 11:44:47 +0300 Subject: [PATCH 02/52] Render polls in statuses --- .../web/mastodon_api/views/status_view.ex | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index bd2372944..6337c50e8 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -232,6 +232,7 @@ def render("status.json", %{activity: %{data: %{"object" => _object}} = activity spoiler_text: summary_html, visibility: get_visibility(object), media_attachments: attachments, + poll: render("poll.json", %{object: object, for: opts[:for]}), mentions: mentions, tags: build_tags(tags), application: %{ @@ -321,6 +322,57 @@ def render("attachment.json", %{attachment: attachment}) do } end + # TODO: Add tests for this view + def render("poll.json", %{object: object} = opts) do + {multiple, options} = + case object.data do + %{"anyOf" => options} when is_list(options) -> {true, options} + %{"oneOf" => options} when is_list(options) -> {false, options} + _ -> {nil, nil} + end + + if options do + end_time = + (object.data["closed"] || object.data["endTime"]) + |> NaiveDateTime.from_iso8601!() + + votes_count = object.data["votes_count"] || 0 + + expired = + end_time + |> NaiveDateTime.compare(NaiveDateTime.utc_now()) + |> case do + :lt -> true + _ -> false + end + + options = + Enum.map(options, fn %{"name" => name} = option -> + name = + HTML.filter_tags( + name, + User.html_filter_policy(opts[:for]) + ) + + %{title: name, votes_count: option["replies"]["votes_count"] || 0} + end) + + %{ + # Mastodon uses separate ids for polls, but an object can't have more than one poll embedded so object id is fine + id: object.id, + expires_at: Utils.to_masto_date(end_time), + expired: expired, + multiple: multiple, + votes_count: votes_count, + options: options, + voted: false, + emojis: build_emojis(object.data["emoji"]) + } + else + nil + end + end + def get_reply_to(activity, %{replied_to_activities: replied_to_activities}) do object = Object.normalize(activity) From fd920c897339b9cedea042dd6698d14380cedae7 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Sat, 18 May 2019 13:29:28 +0300 Subject: [PATCH 03/52] Mastodon API: Add support for posting polls --- lib/pleroma/web/common_api/common_api.ex | 4 +- lib/pleroma/web/common_api/utils.ex | 64 ++++++++++++++++--- .../mastodon_api_controller_test.exs | 22 +++++++ test/web/mastodon_api/status_view_test.exs | 1 + 4 files changed, 82 insertions(+), 9 deletions(-) diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index b53869c75..335ae70b0 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -149,6 +149,7 @@ def post(user, %{"status" => status} = data) do data, visibility ), + {poll, mentions, tags} <- make_poll_data(data, mentions, tags), {to, cc} <- to_for_user_and_mentions(user, mentions, in_reply_to, visibility), context <- make_context(in_reply_to), cw <- data["spoiler_text"] || "", @@ -164,7 +165,8 @@ def post(user, %{"status" => status} = data) do in_reply_to, tags, cw, - cc + cc, + poll ), object <- Map.put( diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index 1dfe50b40..13cdffbbd 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -102,6 +102,48 @@ def to_for_user_and_mentions(_user, mentions, inReplyTo, "direct") do end end + def make_poll_data( + %{"poll" => %{"options" => options, "expires_in" => expires_in}} = data, + mentions, + tags + ) + when is_list(options) and is_integer(expires_in) do + content_type = get_content_type(data["content_type"]) + # XXX: There is probably a more performant/cleaner way to do this + {poll, {mentions, tags}} = + Enum.map_reduce(options, {mentions, tags}, fn option, {mentions, tags} -> + # TODO: Custom emoji + {option, mentions_merge, tags_merge} = format_input(option, content_type) + mentions = mentions ++ mentions_merge + tags = tags ++ tags_merge + + {%{ + "name" => option, + "type" => "Note", + "replies" => %{"type" => "Collection", "totalItems" => 0} + }, {mentions, tags}} + end) + + end_time = + NaiveDateTime.utc_now() + |> NaiveDateTime.add(expires_in) + |> NaiveDateTime.to_iso8601() + + poll = + if Pleroma.Web.ControllerHelper.truthy_param?(data["poll"]["multiple"]) do + %{"type" => "Question", "anyOf" => poll, "closed" => end_time} + else + %{"type" => "Question", "oneOf" => poll, "closed" => end_time} + end + + {poll, mentions, tags} + end + + def make_poll_data(data, mentions, tags) do + IO.inspect(data, label: "data") + {%{}, mentions, tags} + end + def make_content_html( status, attachments, @@ -223,8 +265,11 @@ def make_note_data( in_reply_to, tags, cw \\ nil, - cc \\ [] + cc \\ [], + merge \\ %{} ) do + IO.inspect(merge, label: "merge") + object = %{ "type" => "Note", "to" => to, @@ -237,14 +282,17 @@ def make_note_data( "tag" => tags |> Enum.map(fn {_, tag} -> tag end) |> Enum.uniq() } - if in_reply_to do - in_reply_to_object = Object.normalize(in_reply_to) + object = + if in_reply_to do + in_reply_to_object = Object.normalize(in_reply_to) - object - |> Map.put("inReplyTo", in_reply_to_object.data["id"]) - else - object - end + object + |> Map.put("inReplyTo", in_reply_to_object.data["id"]) + else + object + end + + Map.merge(object, merge) end def format_naive_asctime(date) do diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs index 505e45010..ce581c092 100644 --- a/test/web/mastodon_api/mastodon_api_controller_test.exs +++ b/test/web/mastodon_api/mastodon_api_controller_test.exs @@ -132,6 +132,28 @@ test "posting a status", %{conn: conn} do refute id == third_id end + test "posting a poll", %{conn: conn} do + user = insert(:user) + time = NaiveDateTime.utc_now() + + conn = + conn + |> assign(:user, user) + |> post("/api/v1/statuses", %{ + "status" => "Who is the best girl?", + "poll" => %{"options" => ["Rei", "Asuka", "Misato"], "expires_in" => 420} + }) + + response = json_response(conn, 200) + + assert Enum.all?(response["poll"]["options"], fn %{"title" => title} -> + title in ["Rei", "Asuka", "Misato"] + end) + + assert NaiveDateTime.diff(NaiveDateTime.from_iso8601!(response["poll"]["expires_at"]), time) in 420..430 + refute response["poll"]["expred"] + end + test "posting a sensitive status", %{conn: conn} do user = insert(:user) diff --git a/test/web/mastodon_api/status_view_test.exs b/test/web/mastodon_api/status_view_test.exs index d7c800e83..9f2ebda4e 100644 --- a/test/web/mastodon_api/status_view_test.exs +++ b/test/web/mastodon_api/status_view_test.exs @@ -103,6 +103,7 @@ test "a note activity" do muted: false, pinned: false, sensitive: false, + poll: nil, spoiler_text: HtmlSanitizeEx.basic_html(note.data["object"]["summary"]), visibility: "public", media_attachments: [], From 1d90f9b96999f8bad3fa3e3ec58bf50c8666be4f Mon Sep 17 00:00:00 2001 From: rinpatch Date: Sun, 19 May 2019 17:06:44 +0300 Subject: [PATCH 04/52] Remove tags/mentions/rich text from poll options because Mastodon and add custom emoji --- lib/pleroma/web/common_api/common_api.ex | 4 +-- lib/pleroma/web/common_api/utils.ex | 25 ++++++------------- .../web/mastodon_api/views/status_view.ex | 14 +++++------ 3 files changed, 15 insertions(+), 28 deletions(-) diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index bc8f80389..374967a1b 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -154,7 +154,7 @@ def post(user, %{"status" => status} = data) do data, visibility ), - {poll, mentions, tags} <- make_poll_data(data, mentions, tags), + {poll, poll_emoji} <- make_poll_data(data), {to, cc} <- to_for_user_and_mentions(user, mentions, in_reply_to, visibility), context <- make_context(in_reply_to), cw <- data["spoiler_text"] || "", @@ -179,7 +179,7 @@ def post(user, %{"status" => status} = data) do Map.put( object, "emoji", - Formatter.get_emoji_map(full_payload) + Map.merge(Formatter.get_emoji_map(full_payload), poll_emoji) ) do res = ActivityPub.create( diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index 2ea997789..cd8483c11 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -102,26 +102,15 @@ def to_for_user_and_mentions(_user, mentions, inReplyTo, "direct") do end end - def make_poll_data( - %{"poll" => %{"options" => options, "expires_in" => expires_in}} = data, - mentions, - tags - ) + def make_poll_data(%{"poll" => %{"options" => options, "expires_in" => expires_in}} = data) when is_list(options) and is_integer(expires_in) do - content_type = get_content_type(data["content_type"]) - # XXX: There is probably a more performant/cleaner way to do this - {poll, {mentions, tags}} = - Enum.map_reduce(options, {mentions, tags}, fn option, {mentions, tags} -> - # TODO: Custom emoji - {option, mentions_merge, tags_merge} = format_input(option, content_type) - mentions = mentions ++ mentions_merge - tags = tags ++ tags_merge - + {poll, emoji} = + Enum.map_reduce(options, %{}, fn option, emoji -> {%{ "name" => option, "type" => "Note", "replies" => %{"type" => "Collection", "totalItems" => 0} - }, {mentions, tags}} + }, Map.merge(emoji, Formatter.get_emoji_map(option))} end) end_time = @@ -136,11 +125,11 @@ def make_poll_data( %{"type" => "Question", "oneOf" => poll, "closed" => end_time} end - {poll, mentions, tags} + {poll, emoji} end - def make_poll_data(_data, mentions, tags) do - {%{}, mentions, tags} + def make_poll_data(_data) do + {%{}, %{}} end def make_content_html( diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 2a5691e1f..0df8bb5c2 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -325,7 +325,7 @@ def render("attachment.json", %{attachment: attachment}) do end # TODO: Add tests for this view - def render("poll.json", %{object: object} = opts) do + def render("poll.json", %{object: object} = _opts) do {multiple, options} = case object.data do %{"anyOf" => options} when is_list(options) -> {true, options} @@ -350,13 +350,10 @@ def render("poll.json", %{object: object} = opts) do options = Enum.map(options, fn %{"name" => name} = option -> - name = - HTML.filter_tags( - name, - User.html_filter_policy(opts[:for]) - ) - - %{title: name, votes_count: option["replies"]["votes_count"] || 0} + %{ + title: HTML.strip_tags(name), + votes_count: option["replies"]["votes_count"] || 0 + } end) %{ @@ -367,6 +364,7 @@ def render("poll.json", %{object: object} = opts) do multiple: multiple, votes_count: votes_count, options: options, + # TODO: Actually check for a vote voted: false, emojis: build_emojis(object.data["emoji"]) } From 6430cb1bf78e7949cc023a30df7a8d1547c36524 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Sun, 19 May 2019 17:44:18 +0300 Subject: [PATCH 05/52] Restrict poll replies from fetch queries by default --- lib/pleroma/web/activity_pub/activity_pub.ex | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 5c3156978..035fb75d5 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -820,6 +820,16 @@ defp restrict_muted_reblogs(query, %{"muting_user" => %User{info: info}}) do defp restrict_muted_reblogs(query, _), do: query + defp restrict_poll_replies(query, %{"include_poll_replies" => "true"}), do: query + + defp restrict_poll_replies(query, _) do + if has_named_binding?(query, :object) do + from([activity, object: o] in query, where: fragment("?->'name' is null", o.data)) + else + query + end + end + defp maybe_preload_objects(query, %{"skip_preload" => true}), do: query defp maybe_preload_objects(query, _) do @@ -873,6 +883,7 @@ def fetch_activities_query(recipients, opts \\ %{}) do |> restrict_pinned(opts) |> restrict_muted_reblogs(opts) |> Activity.restrict_deactivated_users() + |> restrict_poll_replies(opts) end def fetch_activities(recipients, opts \\ %{}) do From 76a7429befb2e9a819b653ff8328cc42a565c29d Mon Sep 17 00:00:00 2001 From: rinpatch Date: Tue, 21 May 2019 09:13:10 +0300 Subject: [PATCH 06/52] Add poll limits to /api/v1/instance and initial state --- config/config.exs | 6 ++++++ docs/config.md | 5 +++++ lib/pleroma/web/mastodon_api/mastodon_api_controller.ex | 4 +++- test/web/mastodon_api/mastodon_api_controller_test.exs | 3 ++- 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/config/config.exs b/config/config.exs index 9a10b0ff7..47d8dfb42 100644 --- a/config/config.exs +++ b/config/config.exs @@ -211,6 +211,12 @@ avatar_upload_limit: 2_000_000, background_upload_limit: 4_000_000, banner_upload_limit: 4_000_000, + poll_limits: %{ + max_options: 20, + max_option_chars: 200, + min_expiration: 0, + max_expiration: 365 * 24 * 60 * 60 + }, registrations_open: true, federating: true, federation_reachability_timeout_days: 7, diff --git a/docs/config.md b/docs/config.md index 450d73fda..f9903332c 100644 --- a/docs/config.md +++ b/docs/config.md @@ -71,6 +71,11 @@ config :pleroma, Pleroma.Emails.Mailer, * `avatar_upload_limit`: File size limit of user’s profile avatars * `background_upload_limit`: File size limit of user’s profile backgrounds * `banner_upload_limit`: File size limit of user’s profile banners +* `poll_limits`: A map with poll limits for **local** polls + * `max_options`: Maximum number of options + * `max_option_chars`: Maximum number of characters per option + * `min_expiration`: Minimum expiration time (in seconds) + * `max_expiration`: Maximum expiration time (in seconds) * `registrations_open`: Enable registrations for anyone, invitations can be enabled when false. * `invites_enabled`: Enable user invitations for admins (depends on `registrations_open: false`). * `account_activation_required`: Require users to confirm their emails before signing in. diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index 1051861ff..81cc5a972 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -197,7 +197,8 @@ def masto_instance(conn, _params) do languages: ["en"], registrations: Pleroma.Config.get([:instance, :registrations_open]), # Extra (not present in Mastodon): - max_toot_chars: Keyword.get(instance, :limit) + max_toot_chars: Keyword.get(instance, :limit), + poll_limits: Keyword.get(instance, :poll_limits) } json(conn, response) @@ -1331,6 +1332,7 @@ def index(%{assigns: %{user: user}} = conn, _params) do max_toot_chars: limit, mascot: "/images/pleroma-fox-tan-smol.png" }, + poll_limits: Config.get([:instance, :poll_limits]), rights: %{ delete_others_notice: present?(user.info.is_moderator), admin: present?(user.info.is_admin) diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs index 68fe9c1b4..48268d4f7 100644 --- a/test/web/mastodon_api/mastodon_api_controller_test.exs +++ b/test/web/mastodon_api/mastodon_api_controller_test.exs @@ -2494,7 +2494,8 @@ test "get instance information", %{conn: conn} do "stats" => _, "thumbnail" => _, "languages" => _, - "registrations" => _ + "registrations" => _, + "poll_limits" => _ } = result assert email == from_config_email From 3f96b3e4b8114ec1cf924d452907b17c2aea2003 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Tue, 21 May 2019 10:54:20 +0300 Subject: [PATCH 07/52] Enforce poll limits and add error handling for MastodonAPI's post endpoint --- lib/pleroma/web/common_api/utils.ex | 71 ++++++++---- .../mastodon_api/mastodon_api_controller.ex | 43 ++++--- .../mastodon_api_controller_test.exs | 107 +++++++++++++++--- 3 files changed, 173 insertions(+), 48 deletions(-) diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index cd8483c11..97172fd94 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -104,28 +104,61 @@ def to_for_user_and_mentions(_user, mentions, inReplyTo, "direct") do def make_poll_data(%{"poll" => %{"options" => options, "expires_in" => expires_in}} = data) when is_list(options) and is_integer(expires_in) do - {poll, emoji} = - Enum.map_reduce(options, %{}, fn option, emoji -> - {%{ - "name" => option, - "type" => "Note", - "replies" => %{"type" => "Collection", "totalItems" => 0} - }, Map.merge(emoji, Formatter.get_emoji_map(option))} - end) + %{max_expiration: max_expiration, min_expiration: min_expiration} = + limits = Pleroma.Config.get([:instance, :poll_limits]) - end_time = - NaiveDateTime.utc_now() - |> NaiveDateTime.add(expires_in) - |> NaiveDateTime.to_iso8601() - - poll = - if Pleroma.Web.ControllerHelper.truthy_param?(data["poll"]["multiple"]) do - %{"type" => "Question", "anyOf" => poll, "closed" => end_time} - else - %{"type" => "Question", "oneOf" => poll, "closed" => end_time} + # XXX: There is probably a cleaner way of doing this + try do + if Enum.count(options) > limits.max_options do + raise ArgumentError, message: "Poll can't contain more than #{limits.max_options} options" end - {poll, emoji} + {poll, emoji} = + Enum.map_reduce(options, %{}, fn option, emoji -> + if String.length(option) > limits.max_option_chars do + raise ArgumentError, + message: + "Poll options cannot be longer than #{limits.max_option_chars} characters each" + end + + {%{ + "name" => option, + "type" => "Note", + "replies" => %{"type" => "Collection", "totalItems" => 0} + }, Map.merge(emoji, Formatter.get_emoji_map(option))} + end) + + case expires_in do + expires_in when expires_in > max_expiration -> + raise ArgumentError, message: "Expiration date is too far in the future" + + expires_in when expires_in < min_expiration -> + raise ArgumentError, message: "Expiration date is too soon" + + _ -> + :noop + end + + end_time = + NaiveDateTime.utc_now() + |> NaiveDateTime.add(expires_in) + |> NaiveDateTime.to_iso8601() + + poll = + if Pleroma.Web.ControllerHelper.truthy_param?(data["poll"]["multiple"]) do + %{"type" => "Question", "anyOf" => poll, "closed" => end_time} + else + %{"type" => "Question", "oneOf" => poll, "closed" => end_time} + end + + {poll, emoji} + rescue + e in ArgumentError -> e.message + end + end + + def make_poll_data(%{"poll" => _}) do + "Invalid poll" end def make_poll_data(_data) do diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index 81cc5a972..e2cab86f1 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -473,12 +473,6 @@ def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do params |> Map.put("in_reply_to_status_id", params["in_reply_to_id"]) - idempotency_key = - case get_req_header(conn, "idempotency-key") do - [key] -> key - _ -> Ecto.UUID.generate() - end - scheduled_at = params["scheduled_at"] if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do @@ -491,17 +485,40 @@ def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do else params = Map.drop(params, ["scheduled_at"]) - {:ok, activity} = - Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ -> - CommonAPI.post(user, params) - end) + case get_cached_status_or_post(conn, params) do + {:ignore, message} -> + conn + |> put_status(401) + |> json(%{error: message}) - conn - |> put_view(StatusView) - |> try_render("status.json", %{activity: activity, for: user, as: :activity}) + {:error, message} -> + conn + |> put_status(401) + |> json(%{error: message}) + + {_, activity} -> + conn + |> put_view(StatusView) + |> try_render("status.json", %{activity: activity, for: user, as: :activity}) + end end end + defp get_cached_status_or_post(%{assigns: %{user: user}} = conn, params) do + idempotency_key = + case get_req_header(conn, "idempotency-key") do + [key] -> key + _ -> Ecto.UUID.generate() + end + + Cachex.fetch(:idempotency_cache, idempotency_key, fn _ -> + case CommonAPI.post(user, params) do + {:ok, activity} -> activity + {:error, message} -> {:ignore, message} + end + end) + end + def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do json(conn, %{}) diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs index 48268d4f7..e1df79ffb 100644 --- a/test/web/mastodon_api/mastodon_api_controller_test.exs +++ b/test/web/mastodon_api/mastodon_api_controller_test.exs @@ -146,26 +146,101 @@ test "posting a status", %{conn: conn} do refute id == third_id end - test "posting a poll", %{conn: conn} do - user = insert(:user) - time = NaiveDateTime.utc_now() + describe "posting polls" do + test "posting a poll", %{conn: conn} do + user = insert(:user) + time = NaiveDateTime.utc_now() - conn = - conn - |> assign(:user, user) - |> post("/api/v1/statuses", %{ - "status" => "Who is the best girl?", - "poll" => %{"options" => ["Rei", "Asuka", "Misato"], "expires_in" => 420} - }) + conn = + conn + |> assign(:user, user) + |> post("/api/v1/statuses", %{ + "status" => "Who is the #bestgrill?", + "poll" => %{"options" => ["Rei", "Asuka", "Misato"], "expires_in" => 420} + }) - response = json_response(conn, 200) + response = json_response(conn, 200) - assert Enum.all?(response["poll"]["options"], fn %{"title" => title} -> - title in ["Rei", "Asuka", "Misato"] - end) + assert Enum.all?(response["poll"]["options"], fn %{"title" => title} -> + title in ["Rei", "Asuka", "Misato"] + end) - assert NaiveDateTime.diff(NaiveDateTime.from_iso8601!(response["poll"]["expires_at"]), time) in 420..430 - refute response["poll"]["expred"] + assert NaiveDateTime.diff(NaiveDateTime.from_iso8601!(response["poll"]["expires_at"]), time) in 420..430 + refute response["poll"]["expred"] + end + + test "option limit is enforced", %{conn: conn} do + user = insert(:user) + limit = Pleroma.Config.get([:instance, :poll_limits, :max_options]) + + conn = + conn + |> assign(:user, user) + |> post("/api/v1/statuses", %{ + "status" => "desu~", + "poll" => %{"options" => Enum.map(0..limit, fn _ -> "desu" end), "expires_in" => 1} + }) + + %{"error" => error} = json_response(conn, 401) + assert error == "Poll can't contain more than #{limit} options" + end + + test "option character limit is enforced", %{conn: conn} do + user = insert(:user) + limit = Pleroma.Config.get([:instance, :poll_limits, :max_option_chars]) + + conn = + conn + |> assign(:user, user) + |> post("/api/v1/statuses", %{ + "status" => "...", + "poll" => %{ + "options" => [Enum.reduce(0..limit, "", fn _, acc -> acc <> "." end)], + "expires_in" => 1 + } + }) + + %{"error" => error} = json_response(conn, 401) + assert error == "Poll options cannot be longer than #{limit} characters each" + end + + test "minimal date limit is enforced", %{conn: conn} do + user = insert(:user) + limit = Pleroma.Config.get([:instance, :poll_limits, :min_expiration]) + + conn = + conn + |> assign(:user, user) + |> post("/api/v1/statuses", %{ + "status" => "imagine arbitrary limits", + "poll" => %{ + "options" => ["this post was made by pleroma gang"], + "expires_in" => limit - 1 + } + }) + + %{"error" => error} = json_response(conn, 401) + assert error == "Expiration date is too soon" + end + + test "maximum date limit is enforced", %{conn: conn} do + user = insert(:user) + limit = Pleroma.Config.get([:instance, :poll_limits, :max_expiration]) + + conn = + conn + |> assign(:user, user) + |> post("/api/v1/statuses", %{ + "status" => "imagine arbitrary limits", + "poll" => %{ + "options" => ["this post was made by pleroma gang"], + "expires_in" => limit + 1 + } + }) + + %{"error" => error} = json_response(conn, 401) + assert error == "Expiration date is too far in the future" + end end test "posting a sensitive status", %{conn: conn} do From aafe30d94e68ccf251c56139d07bda154dde3af9 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Tue, 21 May 2019 14:12:10 +0300 Subject: [PATCH 08/52] Handle poll votes --- lib/pleroma/object.ex | 30 ++++++++++++++++++ lib/pleroma/web/activity_pub/activity_pub.ex | 10 ++++++ test/fixtures/mastodon-vote.json | 16 ++++++++++ test/web/activity_pub/transmogrifier_test.exs | 31 +++++++++++++++++++ 4 files changed, 87 insertions(+) create mode 100644 test/fixtures/mastodon-vote.json diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex index 740d687a3..a0f7659eb 100644 --- a/lib/pleroma/object.ex +++ b/lib/pleroma/object.ex @@ -188,4 +188,34 @@ def decrease_replies_count(ap_id) do _ -> {:error, "Not found"} end end + + def increase_vote_count(ap_id, name) do + with %Object{} = object <- Object.normalize(ap_id), + "Question" <- object.data["type"] do + multiple = Map.has_key?(object.data, "anyOf") + + options = + (object.data["anyOf"] || object.data["oneOf"] || []) + |> Enum.map(fn + %{"name" => ^name} = option -> + Kernel.update_in(option["replies"]["totalItems"], &(&1 + 1)) + + option -> + option + end) + + data = + if multiple do + Map.put(object.data, "anyOf", options) + else + Map.put(object.data, "oneOf", options) + end + + object + |> Object.change(%{data: data}) + |> update_and_set_cache() + else + _ -> :noop + end + end end diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 035fb75d5..ce78d895b 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -108,6 +108,15 @@ def decrease_replies_count_if_reply(%Object{ def decrease_replies_count_if_reply(_object), do: :noop + def increase_poll_votes_if_vote(%{ + "object" => %{"inReplyTo" => reply_ap_id, "name" => name}, + "type" => "Create" + }) do + Object.increase_vote_count(reply_ap_id, name) + end + + def increase_poll_votes_if_vote(_create_data), do: :noop + def insert(map, local \\ true, fake \\ false) when is_map(map) do with nil <- Activity.normalize(map), map <- lazy_put_activity_defaults(map, fake), @@ -235,6 +244,7 @@ def create(%{to: to, actor: actor, context: context, object: object} = params, f {:ok, activity} <- insert(create_data, local, fake), {:fake, false, activity} <- {:fake, fake, activity}, _ <- increase_replies_count_if_reply(create_data), + _ <- increase_poll_votes_if_vote(create_data), # Changing note count prior to enqueuing federation task in order to avoid # race conditions on updating user.info {:ok, _actor} <- increase_note_count_if_public(actor, activity), diff --git a/test/fixtures/mastodon-vote.json b/test/fixtures/mastodon-vote.json new file mode 100644 index 000000000..c2c5f40c0 --- /dev/null +++ b/test/fixtures/mastodon-vote.json @@ -0,0 +1,16 @@ +{ + "@context": "https://www.w3.org/ns/activitystreams", + "actor": "https://mastodon.sdf.org/users/rinpatch", + "id": "https://mastodon.sdf.org/users/rinpatch#votes/387/activity", + "nickname": "rin", + "object": { + "attributedTo": "https://mastodon.sdf.org/users/rinpatch", + "id": "https://mastodon.sdf.org/users/rinpatch#votes/387", + "inReplyTo": "https://testing.uguu.ltd/objects/9d300947-2dcb-445d-8978-9a3b4b84fa14", + "name": "suya..", + "to": "https://testing.uguu.ltd/users/rin", + "type": "Note" + }, + "to": "https://testing.uguu.ltd/users/rin", + "type": "Create" +} diff --git a/test/web/activity_pub/transmogrifier_test.exs b/test/web/activity_pub/transmogrifier_test.exs index 727abbd17..32d94e3e9 100644 --- a/test/web/activity_pub/transmogrifier_test.exs +++ b/test/web/activity_pub/transmogrifier_test.exs @@ -130,6 +130,37 @@ test "it works for incoming questions" do end) end + test "in increments vote counters on question activities" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + "status" => "suya...", + "poll" => %{"options" => ["suya", "suya.", "suya.."], "expires_in" => 10} + }) + + object = Object.normalize(activity) + + data = + File.read!("test/fixtures/mastodon-vote.json") + |> Poison.decode!() + |> Kernel.put_in(["to"], user.ap_id) + |> Kernel.put_in(["object", "inReplyTo"], object.data["id"]) + |> Kernel.put_in(["object", "to"], user.ap_id) + + {:ok, %Activity{local: false}} = Transmogrifier.handle_incoming(data) + + object = Object.get_by_ap_id(object.data["id"]) + + assert Enum.any?( + object.data["oneOf"], + fn + %{"name" => "suya..", "replies" => %{"totalItems" => 1}} -> true + _ -> false + end + ) + end + test "it works for incoming notices with contentMap" do data = File.read!("test/fixtures/mastodon-post-activity-contentmap.json") |> Poison.decode!() From a53d06273080525fdda332291838b0c95ed69690 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Tue, 21 May 2019 14:19:03 +0300 Subject: [PATCH 09/52] Fix posting non-polls from mastofe --- lib/pleroma/web/common_api/utils.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index 97172fd94..66153a105 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -157,7 +157,7 @@ def make_poll_data(%{"poll" => %{"options" => options, "expires_in" => expires_i end end - def make_poll_data(%{"poll" => _}) do + def make_poll_data(%{"poll" => poll}) when is_map(poll) do "Invalid poll" end From f28747858bf3265e8b82eb587919f5a89386bed7 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Tue, 21 May 2019 14:27:09 +0300 Subject: [PATCH 10/52] Actual vote count in poll view --- .../web/mastodon_api/views/status_view.ex | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 0df8bb5c2..c501c213c 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -338,8 +338,6 @@ def render("poll.json", %{object: object} = _opts) do (object.data["closed"] || object.data["endTime"]) |> NaiveDateTime.from_iso8601!() - votes_count = object.data["votes_count"] || 0 - expired = end_time |> NaiveDateTime.compare(NaiveDateTime.utc_now()) @@ -348,12 +346,14 @@ def render("poll.json", %{object: object} = _opts) do _ -> false end - options = - Enum.map(options, fn %{"name" => name} = option -> - %{ - title: HTML.strip_tags(name), - votes_count: option["replies"]["votes_count"] || 0 - } + {options, votes_count} = + Enum.map_reduce(options, 0, fn %{"name" => name} = option, count -> + current_count = option["replies"]["totalItems"] || 0 + + {%{ + title: HTML.strip_tags(name), + votes_count: current_count + }, current_count + count} end) %{ From d7c4d029c82c833278b3640d585235dc704f30d1 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Tue, 21 May 2019 14:35:20 +0300 Subject: [PATCH 11/52] Restrict poll replies when fetching activiites for context --- lib/pleroma/web/activity_pub/activity_pub.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index ce78d895b..c04e6c2b8 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -491,6 +491,7 @@ defp fetch_activities_for_context_query(context, opts) do from(activity in Activity) |> restrict_blocked(opts) + |> restrict_poll_replies(opts) |> restrict_recipients(recipients, opts["user"]) |> where( [activity], From ee682441415d7abe4acb2643e1d76fe8d78e80c1 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Tue, 21 May 2019 16:58:15 +0300 Subject: [PATCH 12/52] Do not stream out poll replies --- lib/pleroma/web/activity_pub/activity_pub.ex | 54 ++++++++++---------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index c04e6c2b8..fdebf1f6b 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -192,40 +192,42 @@ def stream_out(activity) do public = "https://www.w3.org/ns/activitystreams#Public" if activity.data["type"] in ["Create", "Announce", "Delete"] do - Pleroma.Web.Streamer.stream("user", activity) - Pleroma.Web.Streamer.stream("list", activity) + object = Object.normalize(activity) + # Do not stream out poll replies + unless object.data["name"] do + Pleroma.Web.Streamer.stream("user", activity) + Pleroma.Web.Streamer.stream("list", activity) - if Enum.member?(activity.data["to"], public) do - Pleroma.Web.Streamer.stream("public", activity) + if Enum.member?(activity.data["to"], public) do + Pleroma.Web.Streamer.stream("public", activity) - if activity.local do - Pleroma.Web.Streamer.stream("public:local", activity) - end + if activity.local do + Pleroma.Web.Streamer.stream("public:local", activity) + end - if activity.data["type"] in ["Create"] do - object = Object.normalize(activity) + if activity.data["type"] in ["Create"] do + object.data + |> Map.get("tag", []) + |> Enum.filter(fn tag -> is_bitstring(tag) end) + |> Enum.each(fn tag -> Pleroma.Web.Streamer.stream("hashtag:" <> tag, activity) end) - object.data - |> Map.get("tag", []) - |> Enum.filter(fn tag -> is_bitstring(tag) end) - |> Enum.each(fn tag -> Pleroma.Web.Streamer.stream("hashtag:" <> tag, activity) end) + if object.data["attachment"] != [] do + Pleroma.Web.Streamer.stream("public:media", activity) - if object.data["attachment"] != [] do - Pleroma.Web.Streamer.stream("public:media", activity) - - if activity.local do - Pleroma.Web.Streamer.stream("public:local:media", activity) + if activity.local do + Pleroma.Web.Streamer.stream("public:local:media", activity) + end end end + else + # TODO: Write test, replace with visibility test + if !Enum.member?(activity.data["cc"] || [], public) && + !Enum.member?( + activity.data["to"], + User.get_cached_by_ap_id(activity.data["actor"]).follower_address + ), + do: Pleroma.Web.Streamer.stream("direct", activity) end - else - # TODO: Write test, replace with visibility test - if !Enum.member?(activity.data["cc"] || [], public) && - !Enum.member?( - activity.data["to"], - User.get_cached_by_ap_id(activity.data["actor"]).follower_address - ), - do: Pleroma.Web.Streamer.stream("direct", activity) end end end From 0407ffe75f7e91db240d491492eadf1385b1726b Mon Sep 17 00:00:00 2001 From: rinpatch Date: Tue, 21 May 2019 17:12:38 +0300 Subject: [PATCH 13/52] Change validation error status codes to be more appropriate --- lib/pleroma/web/mastodon_api/mastodon_api_controller.ex | 4 ++-- test/web/mastodon_api/mastodon_api_controller_test.exs | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index e2cab86f1..aef2abf0b 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -488,12 +488,12 @@ def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do case get_cached_status_or_post(conn, params) do {:ignore, message} -> conn - |> put_status(401) + |> put_status(422) |> json(%{error: message}) {:error, message} -> conn - |> put_status(401) + |> put_status(422) |> json(%{error: message}) {_, activity} -> diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs index e1df79ffb..4f332f83c 100644 --- a/test/web/mastodon_api/mastodon_api_controller_test.exs +++ b/test/web/mastodon_api/mastodon_api_controller_test.exs @@ -181,7 +181,7 @@ test "option limit is enforced", %{conn: conn} do "poll" => %{"options" => Enum.map(0..limit, fn _ -> "desu" end), "expires_in" => 1} }) - %{"error" => error} = json_response(conn, 401) + %{"error" => error} = json_response(conn, 422) assert error == "Poll can't contain more than #{limit} options" end @@ -200,7 +200,7 @@ test "option character limit is enforced", %{conn: conn} do } }) - %{"error" => error} = json_response(conn, 401) + %{"error" => error} = json_response(conn, 422) assert error == "Poll options cannot be longer than #{limit} characters each" end @@ -219,7 +219,7 @@ test "minimal date limit is enforced", %{conn: conn} do } }) - %{"error" => error} = json_response(conn, 401) + %{"error" => error} = json_response(conn, 422) assert error == "Expiration date is too soon" end @@ -238,7 +238,7 @@ test "maximum date limit is enforced", %{conn: conn} do } }) - %{"error" => error} = json_response(conn, 401) + %{"error" => error} = json_response(conn, 422) assert error == "Expiration date is too far in the future" end end From 5f67c26baf2bdc98480fa4cda5895f33351b13ab Mon Sep 17 00:00:00 2001 From: rinpatch Date: Tue, 21 May 2019 17:30:51 +0300 Subject: [PATCH 14/52] Accept strings in expires_in because sasuga javascript --- lib/pleroma/web/common_api/utils.ex | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index 66153a105..1a239de97 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -103,12 +103,15 @@ def to_for_user_and_mentions(_user, mentions, inReplyTo, "direct") do end def make_poll_data(%{"poll" => %{"options" => options, "expires_in" => expires_in}} = data) - when is_list(options) and is_integer(expires_in) do + when is_list(options) do %{max_expiration: max_expiration, min_expiration: min_expiration} = limits = Pleroma.Config.get([:instance, :poll_limits]) # XXX: There is probably a cleaner way of doing this try do + # In some cases mastofe sends out strings instead of integers + expires_in = if is_binary(expires_in), do: String.to_integer(expires_in), else: expires_in + if Enum.count(options) > limits.max_options do raise ArgumentError, message: "Poll can't contain more than #{limits.max_options} options" end From ff61d345020b9ee79b243fb8d23a37a06cc41b8e Mon Sep 17 00:00:00 2001 From: rinpatch Date: Tue, 21 May 2019 17:33:54 +0300 Subject: [PATCH 15/52] Accept question objects for conversations --- lib/pleroma/conversation.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/conversation.ex b/lib/pleroma/conversation.ex index 238c1acf2..bc97b39ca 100644 --- a/lib/pleroma/conversation.ex +++ b/lib/pleroma/conversation.ex @@ -49,7 +49,7 @@ def create_or_bump_for(activity, opts \\ []) do with true <- Pleroma.Web.ActivityPub.Visibility.is_direct?(activity), "Create" <- activity.data["type"], object <- Pleroma.Object.normalize(activity), - "Note" <- object.data["type"], + true <- object.data["type"] in ["Note", "Question"], ap_id when is_binary(ap_id) and byte_size(ap_id) > 0 <- object.data["context"] do {:ok, conversation} = create_for_ap_id(ap_id) From 63b0b7190cb652cd27d236e3f9daaaf5e50701a6 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Tue, 21 May 2019 20:40:35 +0300 Subject: [PATCH 16/52] MastoAPI: Add GET /api/v1/polls/:id --- lib/pleroma/object.ex | 3 ++ .../mastodon_api/mastodon_api_controller.ex | 20 +++++++++ lib/pleroma/web/router.ex | 2 + .../mastodon_api_controller_test.exs | 44 +++++++++++++++++++ 4 files changed, 69 insertions(+) diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex index a0f7659eb..3ffa290eb 100644 --- a/lib/pleroma/object.ex +++ b/lib/pleroma/object.ex @@ -35,6 +35,9 @@ def change(struct, params \\ %{}) do |> unique_constraint(:ap_id, name: :objects_unique_apid_index) end + def get_by_id(nil), do: nil + def get_by_id(id), do: Repo.get(Object, id) + def get_by_ap_id(nil), do: nil def get_by_ap_id(ap_id) do diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index aef2abf0b..ecb7df459 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -410,6 +410,26 @@ def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do end end + def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do + with %Object{} = object <- Object.get_by_id(id), + %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]), + true <- Visibility.visible_for_user?(activity, user) do + conn + |> put_view(StatusView) + |> try_render("poll.json", %{object: object, for: user}) + else + nil -> + conn + |> put_status(404) + |> json(%{error: "Record not found"}) + + false -> + conn + |> put_status(404) + |> json(%{error: "Record not found"}) + end + end + def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do conn diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 6a4e4a1d4..e0611e3fc 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -423,6 +423,8 @@ defmodule Pleroma.Web.Router do get("/statuses/:id", MastodonAPIController, :get_status) get("/statuses/:id/context", MastodonAPIController, :get_context) + get("/polls/:id", MastodonAPIController, :get_poll) + get("/accounts/:id/statuses", MastodonAPIController, :user_statuses) get("/accounts/:id/followers", MastodonAPIController, :followers) get("/accounts/:id/following", MastodonAPIController, :following) diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs index 4f332f83c..0d56b6ff2 100644 --- a/test/web/mastodon_api/mastodon_api_controller_test.exs +++ b/test/web/mastodon_api/mastodon_api_controller_test.exs @@ -3453,4 +3453,48 @@ test "rate limit", %{conn: conn} do assert json_response(conn, 403) == %{"error" => "Rate limit exceeded."} end end + + describe "GET /api/v1/polls/:id" do + test "returns poll entity for object id", %{conn: conn} do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + "status" => "Pleroma does", + "poll" => %{"options" => ["what Mastodon't", "n't what Mastodoes"], "expires_in" => 20} + }) + + object = Object.normalize(activity) + + conn = + conn + |> assign(:user, user) + |> get("/api/v1/polls/#{object.id}") + + response = json_response(conn, 200) + id = object.id + assert %{"id" => ^id, "expired" => false, "multiple" => false} = response + end + + test "does not expose polls for private statuses", %{conn: conn} do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + "status" => "Pleroma does", + "poll" => %{"options" => ["what Mastodon't", "n't what Mastodoes"], "expires_in" => 20}, + "visibility" => "private" + }) + + object = Object.normalize(activity) + + conn = + conn + |> assign(:user, other_user) + |> get("/api/v1/polls/#{object.id}") + + assert json_response(conn, 404) + end + end end From 10ca1f91de12c3ff3a1adff2cf36e9ec47c659dd Mon Sep 17 00:00:00 2001 From: rinpatch Date: Wed, 22 May 2019 11:56:53 +0300 Subject: [PATCH 17/52] Add GIN index on object data->'name' --- .../migrations/20190522084924_add_object_index_on_name.exs | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 priv/repo/migrations/20190522084924_add_object_index_on_name.exs diff --git a/priv/repo/migrations/20190522084924_add_object_index_on_name.exs b/priv/repo/migrations/20190522084924_add_object_index_on_name.exs new file mode 100644 index 000000000..886e8499e --- /dev/null +++ b/priv/repo/migrations/20190522084924_add_object_index_on_name.exs @@ -0,0 +1,7 @@ +defmodule Pleroma.Repo.Migrations.AddObjectIndexOnName do + use Ecto.Migration + + def change do + create(index(:objects, ["(data->'name')"], name: :objects_name_index, using: :gin)) + end +end From 19c90d47c4f649d0962098050f6cbc65eeb889d0 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Wed, 22 May 2019 21:17:57 +0300 Subject: [PATCH 18/52] Normalize poll votes to Answer objects --- lib/pleroma/web/activity_pub/activity_pub.ex | 14 +-------- .../web/activity_pub/transmogrifier.ex | 22 ++++++++++++- lib/pleroma/web/activity_pub/utils.ex | 2 +- test/web/activity_pub/transmogrifier_test.exs | 31 +++++++++++++++++-- 4 files changed, 51 insertions(+), 18 deletions(-) diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index fdebf1f6b..5b3fb7e84 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -194,7 +194,7 @@ def stream_out(activity) do if activity.data["type"] in ["Create", "Announce", "Delete"] do object = Object.normalize(activity) # Do not stream out poll replies - unless object.data["name"] do + unless object.data["type"] == "Answer" do Pleroma.Web.Streamer.stream("user", activity) Pleroma.Web.Streamer.stream("list", activity) @@ -493,7 +493,6 @@ defp fetch_activities_for_context_query(context, opts) do from(activity in Activity) |> restrict_blocked(opts) - |> restrict_poll_replies(opts) |> restrict_recipients(recipients, opts["user"]) |> where( [activity], @@ -833,16 +832,6 @@ defp restrict_muted_reblogs(query, %{"muting_user" => %User{info: info}}) do defp restrict_muted_reblogs(query, _), do: query - defp restrict_poll_replies(query, %{"include_poll_replies" => "true"}), do: query - - defp restrict_poll_replies(query, _) do - if has_named_binding?(query, :object) do - from([activity, object: o] in query, where: fragment("?->'name' is null", o.data)) - else - query - end - end - defp maybe_preload_objects(query, %{"skip_preload" => true}), do: query defp maybe_preload_objects(query, _) do @@ -896,7 +885,6 @@ def fetch_activities_query(recipients, opts \\ %{}) do |> restrict_pinned(opts) |> restrict_muted_reblogs(opts) |> Activity.restrict_deactivated_users() - |> restrict_poll_replies(opts) end def fetch_activities(recipients, opts \\ %{}) do diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 8b2427258..70b467b3f 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -35,6 +35,7 @@ def fix_object(object) do |> fix_likes |> fix_addressing |> fix_summary + |> fix_type end def fix_summary(%{"summary" => nil} = object) do @@ -328,6 +329,18 @@ def fix_content_map(%{"contentMap" => content_map} = object) do def fix_content_map(object), do: object + def fix_type(%{"inReplyTo" => reply_id} = object) when is_binary(reply_id) do + reply = Object.normalize(reply_id) + + if reply.data["type"] == "Question" and object["name"] do + Map.put(object, "type", "Answer") + else + object + end + end + + def fix_type(object), do: object + defp mastodon_follow_hack(%{"id" => id, "actor" => follower_id}, followed) do with true <- id =~ "follows", %User{local: true} = follower <- User.get_cached_by_ap_id(follower_id), @@ -398,7 +411,7 @@ def handle_incoming(%{"id" => id}) when not (is_binary(id) and length(id) > 8), # - tags # - emoji def handle_incoming(%{"type" => "Create", "object" => %{"type" => objtype} = object} = data) - when objtype in ["Article", "Note", "Video", "Page", "Question"] do + when objtype in ["Article", "Note", "Video", "Page", "Question", "Answer"] do actor = Containment.get_actor(data) data = @@ -731,6 +744,7 @@ def prepare_object(object) do |> set_reply_to_uri |> strip_internal_fields |> strip_internal_tags + |> set_type end # @doc @@ -895,6 +909,12 @@ def set_sensitive(object) do Map.put(object, "sensitive", "nsfw" in tags) end + def set_type(%{"type" => "Answer"} = object) do + Map.put(object, "type", "Note") + end + + def set_type(object), do: object + def add_attributed_to(object) do attributed_to = object["attributedTo"] || object["actor"] diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index 63454e3f7..9646bbee9 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -19,7 +19,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do require Logger - @supported_object_types ["Article", "Note", "Video", "Page", "Question"] + @supported_object_types ["Article", "Note", "Video", "Page", "Question", "Answer"] @supported_report_states ~w(open closed resolved) @valid_visibilities ~w(public unlisted private direct) diff --git a/test/web/activity_pub/transmogrifier_test.exs b/test/web/activity_pub/transmogrifier_test.exs index 32d94e3e9..8422fc3d5 100644 --- a/test/web/activity_pub/transmogrifier_test.exs +++ b/test/web/activity_pub/transmogrifier_test.exs @@ -130,7 +130,7 @@ test "it works for incoming questions" do end) end - test "in increments vote counters on question activities" do + test "it rewrites Note votes to Answers and increments vote counters on question activities" do user = insert(:user) {:ok, activity} = @@ -148,8 +148,9 @@ test "in increments vote counters on question activities" do |> Kernel.put_in(["object", "inReplyTo"], object.data["id"]) |> Kernel.put_in(["object", "to"], user.ap_id) - {:ok, %Activity{local: false}} = Transmogrifier.handle_incoming(data) - + {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) + answer_object = Object.normalize(activity) + assert answer_object.data["type"] == "Answer" object = Object.get_by_ap_id(object.data["id"]) assert Enum.any?( @@ -1257,4 +1258,28 @@ test "successfully reserializes a message with AS2 objects in IR" do {:ok, _} = Transmogrifier.prepare_outgoing(activity.data) end end + + test "Rewrites Answers to Notes" do + user = insert(:user) + + {:ok, poll_activity} = + CommonAPI.post(user, %{ + "status" => "suya...", + "poll" => %{"options" => ["suya", "suya.", "suya.."], "expires_in" => 10} + }) + + poll_object = Object.normalize(poll_activity) + # TODO: Replace with CommonAPI vote creation when implemented + data = + File.read!("test/fixtures/mastodon-vote.json") + |> Poison.decode!() + |> Kernel.put_in(["to"], user.ap_id) + |> Kernel.put_in(["object", "inReplyTo"], poll_object.data["id"]) + |> Kernel.put_in(["object", "to"], user.ap_id) + + {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data) + {:ok, data} = Transmogrifier.prepare_outgoing(activity.data) + + assert data["object"]["type"] == "Note" + end end From ac7702f8009bf244a9e77cba39114dbb6584739f Mon Sep 17 00:00:00 2001 From: rinpatch Date: Wed, 22 May 2019 21:49:19 +0300 Subject: [PATCH 19/52] Exclude Answers from fetching by default --- lib/pleroma/web/activity_pub/activity_pub.ex | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 5b3fb7e84..db5a4a7ee 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -492,6 +492,8 @@ defp fetch_activities_for_context_query(context, opts) do if opts["user"], do: [opts["user"].ap_id | opts["user"].following] ++ public, else: public from(activity in Activity) + |> Activity.with_preloaded_object() + |> exclude_poll_votes(opts) |> restrict_blocked(opts) |> restrict_recipients(recipients, opts["user"]) |> where( @@ -832,6 +834,18 @@ defp restrict_muted_reblogs(query, %{"muting_user" => %User{info: info}}) do defp restrict_muted_reblogs(query, _), do: query + defp exclude_poll_votes(query, %{"include_poll_votes" => "true"}), do: query + + defp exclude_poll_votes(query, _) do + if has_named_binding?(query, :object) do + from([activity, object: o] in query, + where: fragment("not(?->>'type' = ?)", o.data, "Answer") + ) + else + query + end + end + defp maybe_preload_objects(query, %{"skip_preload" => true}), do: query defp maybe_preload_objects(query, _) do @@ -865,6 +879,7 @@ def fetch_activities_query(recipients, opts \\ %{}) do |> maybe_preload_objects(opts) |> maybe_preload_bookmarks(opts) |> maybe_order(opts) + |> exclude_poll_votes(opts) |> restrict_recipients(recipients, opts["user"]) |> restrict_tag(opts) |> restrict_tag_reject(opts) From e6b175ed6cafcd6a05120e003cfe40a04b38849f Mon Sep 17 00:00:00 2001 From: rinpatch Date: Wed, 22 May 2019 21:57:46 +0300 Subject: [PATCH 20/52] Fix credo issues --- lib/pleroma/web/mastodon_api/views/status_view.ex | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index c501c213c..7a393a817 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -357,7 +357,8 @@ def render("poll.json", %{object: object} = _opts) do end) %{ - # Mastodon uses separate ids for polls, but an object can't have more than one poll embedded so object id is fine + # Mastodon uses separate ids for polls, but an object can't have + # more than one poll embedded so object id is fine id: object.id, expires_at: Utils.to_masto_date(end_time), expired: expired, From 8b2d39c1ec8f47df8a2159c23eabdc61b983ddf0 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Thu, 23 May 2019 14:03:16 +0300 Subject: [PATCH 21/52] Change the order of preloading when fetching activities for context --- lib/pleroma/web/activity_pub/activity_pub.ex | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index db5a4a7ee..b06200cb5 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -492,7 +492,7 @@ defp fetch_activities_for_context_query(context, opts) do if opts["user"], do: [opts["user"].ap_id | opts["user"].following] ++ public, else: public from(activity in Activity) - |> Activity.with_preloaded_object() + |> maybe_preload_objects(opts) |> exclude_poll_votes(opts) |> restrict_blocked(opts) |> restrict_recipients(recipients, opts["user"]) @@ -513,7 +513,6 @@ defp fetch_activities_for_context_query(context, opts) do def fetch_activities_for_context(context, opts \\ %{}) do context |> fetch_activities_for_context_query(opts) - |> Activity.with_preloaded_object() |> Repo.all() end @@ -521,7 +520,7 @@ def fetch_activities_for_context(context, opts \\ %{}) do Pleroma.FlakeId.t() | nil def fetch_latest_activity_id_for_context(context, opts \\ %{}) do context - |> fetch_activities_for_context_query(opts) + |> fetch_activities_for_context_query(Map.merge(%{"skip_preload" => true}, opts)) |> limit(1) |> select([a], a.id) |> Repo.one() From 4030837d91b7ff8525513589d5d810e9a1a6b959 Mon Sep 17 00:00:00 2001 From: William Pitcock Date: Sat, 25 May 2019 05:19:47 +0000 Subject: [PATCH 22/52] notification: add non_follows/non_followers notification control settings --- lib/pleroma/notification.ex | 32 +++++++++++++++++++++++++++++++- lib/pleroma/user/info.ex | 11 +++++++++-- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index 844264307..4095e0474 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -166,7 +166,17 @@ def get_notified_from_activity( def get_notified_from_activity(_, _local_only), do: [] def skip?(activity, user) do - [:self, :blocked, :local, :muted, :followers, :follows, :recently_followed] + [ + :self, + :blocked, + :local, + :muted, + :followers, + :follows, + :non_followers, + :non_follows, + :recently_followed + ] |> Enum.any?(&skip?(&1, activity, user)) end @@ -201,12 +211,32 @@ def skip?( User.following?(follower, user) end + def skip?( + :non_followers, + activity, + %{info: %{notification_settings: %{"non_followers" => false}}} = user + ) do + actor = activity.data["actor"] + follower = User.get_cached_by_ap_id(actor) + !User.following?(follower, user) + end + def skip?(:follows, activity, %{info: %{notification_settings: %{"follows" => false}}} = user) do actor = activity.data["actor"] followed = User.get_cached_by_ap_id(actor) User.following?(user, followed) end + def skip?( + :non_follows, + activity, + %{info: %{notification_settings: %{"non_follows" => false}}} = user + ) do + actor = activity.data["actor"] + followed = User.get_cached_by_ap_id(actor) + !User.following?(user, followed) + end + def skip?(:recently_followed, %{data: %{"type" => "Follow"}} = activity, user) do actor = activity.data["actor"] diff --git a/lib/pleroma/user/info.ex b/lib/pleroma/user/info.ex index 6397e2737..fb4cf3cc3 100644 --- a/lib/pleroma/user/info.ex +++ b/lib/pleroma/user/info.ex @@ -47,7 +47,14 @@ defmodule Pleroma.User.Info do field(:emoji, {:array, :map}, default: []) field(:notification_settings, :map, - default: %{"remote" => true, "local" => true, "followers" => true, "follows" => true} + default: %{ + "remote" => true, + "local" => true, + "followers" => true, + "follows" => true, + "non_follows" => true, + "non_followers" => true + } ) # Found in the wild @@ -71,7 +78,7 @@ def update_notification_settings(info, settings) do notification_settings = info.notification_settings |> Map.merge(settings) - |> Map.take(["remote", "local", "followers", "follows"]) + |> Map.take(["remote", "local", "followers", "follows", "non_follows", "non_followers"]) params = %{notification_settings: notification_settings} From 1542cccbbcc2935add83d1428b4cd9e4b146f1ec Mon Sep 17 00:00:00 2001 From: William Pitcock Date: Sat, 25 May 2019 05:22:13 +0000 Subject: [PATCH 23/52] tests: chase notification setting changes --- test/web/mastodon_api/account_view_test.exs | 4 +++- test/web/twitter_api/util_controller_test.exs | 10 ++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/test/web/mastodon_api/account_view_test.exs b/test/web/mastodon_api/account_view_test.exs index aaf2261bb..6f8480ee2 100644 --- a/test/web/mastodon_api/account_view_test.exs +++ b/test/web/mastodon_api/account_view_test.exs @@ -81,7 +81,9 @@ test "Represent the user account for the account owner" do "remote" => true, "local" => true, "followers" => true, - "follows" => true + "follows" => true, + "non_follows" => true, + "non_followers" => true } privacy = user.info.default_scope diff --git a/test/web/twitter_api/util_controller_test.exs b/test/web/twitter_api/util_controller_test.exs index 2cd82b3e7..ca0b8cc26 100644 --- a/test/web/twitter_api/util_controller_test.exs +++ b/test/web/twitter_api/util_controller_test.exs @@ -110,8 +110,14 @@ test "it updates notification settings", %{conn: conn} do user = Repo.get(User, user.id) - assert %{"remote" => false, "local" => true, "followers" => false, "follows" => true} == - user.info.notification_settings + assert %{ + "remote" => false, + "local" => true, + "followers" => false, + "follows" => true, + "non_follows" => true, + "non_followers" => true + } == user.info.notification_settings end end From 0f7eeb0943472da80405639f17b9bc99e4fcd417 Mon Sep 17 00:00:00 2001 From: William Pitcock Date: Sat, 25 May 2019 05:25:40 +0000 Subject: [PATCH 24/52] tests: add tests for non-follows/non-followers settings --- test/notification_test.exs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/notification_test.exs b/test/notification_test.exs index 581db58a8..b54414dcd 100644 --- a/test/notification_test.exs +++ b/test/notification_test.exs @@ -113,6 +113,13 @@ test "it disables notifications from followers" do assert nil == Notification.create_notification(activity, followed) end + test "it disables notifications from non-followers" do + follower = insert(:user) + followed = insert(:user, info: %{notification_settings: %{"non_followers" => false}}) + {:ok, activity} = CommonAPI.post(follower, %{"status" => "hey @#{followed.nickname}"}) + assert nil == Notification.create_notification(activity, followed) + end + test "it disables notifications from people the user follows" do follower = insert(:user, info: %{notification_settings: %{"follows" => false}}) followed = insert(:user) @@ -122,6 +129,13 @@ test "it disables notifications from people the user follows" do assert nil == Notification.create_notification(activity, follower) end + test "it disables notifications from people the user does not follow" do + follower = insert(:user, info: %{notification_settings: %{"non_follows" => false}}) + followed = insert(:user) + {:ok, activity} = CommonAPI.post(followed, %{"status" => "hey @#{follower.nickname}"}) + assert nil == Notification.create_notification(activity, follower) + end + test "it doesn't create a notification for user if he is the activity author" do activity = insert(:note_activity) author = User.get_cached_by_ap_id(activity.data["actor"]) From 59a703fcbe6436c92d0e276caaf55f599c3165f4 Mon Sep 17 00:00:00 2001 From: William Pitcock Date: Sat, 25 May 2019 05:31:13 +0000 Subject: [PATCH 25/52] twitter api: user view: expose user notification settings under pleroma object --- lib/pleroma/web/twitter_api/views/user_view.ex | 7 +++++++ test/web/twitter_api/views/user_view_test.exs | 2 ++ 2 files changed, 9 insertions(+) diff --git a/lib/pleroma/web/twitter_api/views/user_view.ex b/lib/pleroma/web/twitter_api/views/user_view.ex index f0a4ddbd3..550f35f5f 100644 --- a/lib/pleroma/web/twitter_api/views/user_view.ex +++ b/lib/pleroma/web/twitter_api/views/user_view.ex @@ -121,6 +121,7 @@ defp do_render("user.json", %{user: user = %User{}} = assigns) do "tags" => user.tags } |> maybe_with_activation_status(user, for_user) + |> with_notification_settings(user, for_user) } |> maybe_with_user_settings(user, for_user) |> maybe_with_role(user, for_user) @@ -132,6 +133,12 @@ defp do_render("user.json", %{user: user = %User{}} = assigns) do end end + defp with_notification_settings(data, %User{id: user_id} = user, %User{id: user_id}) do + Map.put(data, "notification_settings", user.info.notification_settings) + end + + defp with_notification_settings(data, _, _), do: data + defp maybe_with_activation_status(data, user, %User{info: %{is_admin: true}}) do Map.put(data, "deactivated", user.info.deactivated) end diff --git a/test/web/twitter_api/views/user_view_test.exs b/test/web/twitter_api/views/user_view_test.exs index 74526673c..a48fc9b78 100644 --- a/test/web/twitter_api/views/user_view_test.exs +++ b/test/web/twitter_api/views/user_view_test.exs @@ -112,9 +112,11 @@ test "User exposes settings for themselves and only for themselves", %{user: use as_user = UserView.render("show.json", %{user: user, for: user}) assert as_user["default_scope"] == user.info.default_scope assert as_user["no_rich_text"] == user.info.no_rich_text + assert as_user["pleroma"]["notification_settings"] == user.info.notification_settings as_stranger = UserView.render("show.json", %{user: user}) refute as_stranger["default_scope"] refute as_stranger["no_rich_text"] + refute as_stranger["pleroma"]["notification_settings"] end test "A user for a given other follower", %{user: user} do From e7e2e7a1a633e2f7e493d040e290c931320d8cc8 Mon Sep 17 00:00:00 2001 From: William Pitcock Date: Sat, 25 May 2019 05:54:02 +0000 Subject: [PATCH 26/52] user info: allow formdata for notification settings like every other API --- lib/pleroma/user/info.ex | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/pleroma/user/info.ex b/lib/pleroma/user/info.ex index fb4cf3cc3..b0bfdf4f4 100644 --- a/lib/pleroma/user/info.ex +++ b/lib/pleroma/user/info.ex @@ -75,6 +75,11 @@ def set_activation_status(info, deactivated) do end def update_notification_settings(info, settings) do + settings = + settings + |> Enum.map(fn {k, v} -> {k, v in [true, "true", "True", "1"]} end) + |> Map.new() + notification_settings = info.notification_settings |> Map.merge(settings) From 5fbbc57c1b497f8d194f3bbdefd889f0a943525a Mon Sep 17 00:00:00 2001 From: William Pitcock Date: Sat, 25 May 2019 07:25:13 +0000 Subject: [PATCH 27/52] add migration to add notification settings to user accounts --- ...d_non_followers_fields_to_notification_settings.exs | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 priv/repo/migrations/20190525071417_add_non_follows_and_non_followers_fields_to_notification_settings.exs diff --git a/priv/repo/migrations/20190525071417_add_non_follows_and_non_followers_fields_to_notification_settings.exs b/priv/repo/migrations/20190525071417_add_non_follows_and_non_followers_fields_to_notification_settings.exs new file mode 100644 index 000000000..a88b0ea61 --- /dev/null +++ b/priv/repo/migrations/20190525071417_add_non_follows_and_non_followers_fields_to_notification_settings.exs @@ -0,0 +1,10 @@ +defmodule Pleroma.Repo.Migrations.AddNonFollowsAndNonFollowersFieldsToNotificationSettings do + use Ecto.Migration + + def change do + execute(""" + update users set info = jsonb_set(info, '{notification_settings}', '{"local": true, "remote": true, "follows": true, "followers": true, "non_follows": true, "non_followers": true}') + where local=true + """) + end +end From 750ede5764d30063587182696f7ebc50c05f8278 Mon Sep 17 00:00:00 2001 From: William Pitcock Date: Sun, 26 May 2019 00:05:47 +0000 Subject: [PATCH 28/52] notification: remove local/remote match rules (too complicated) --- lib/pleroma/notification.ex | 7 ------- lib/pleroma/user/info.ex | 4 +--- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index 4095e0474..35beffb87 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -169,7 +169,6 @@ def skip?(activity, user) do [ :self, :blocked, - :local, :muted, :followers, :follows, @@ -189,12 +188,6 @@ def skip?(:blocked, activity, user) do User.blocks?(user, %{ap_id: actor}) end - def skip?(:local, %{local: true}, %{info: %{notification_settings: %{"local" => false}}}), - do: true - - def skip?(:local, %{local: false}, %{info: %{notification_settings: %{"remote" => false}}}), - do: true - def skip?(:muted, activity, user) do actor = activity.data["actor"] diff --git a/lib/pleroma/user/info.ex b/lib/pleroma/user/info.ex index b0bfdf4f4..74573ba83 100644 --- a/lib/pleroma/user/info.ex +++ b/lib/pleroma/user/info.ex @@ -48,8 +48,6 @@ defmodule Pleroma.User.Info do field(:notification_settings, :map, default: %{ - "remote" => true, - "local" => true, "followers" => true, "follows" => true, "non_follows" => true, @@ -83,7 +81,7 @@ def update_notification_settings(info, settings) do notification_settings = info.notification_settings |> Map.merge(settings) - |> Map.take(["remote", "local", "followers", "follows", "non_follows", "non_followers"]) + |> Map.take(["followers", "follows", "non_follows", "non_followers"]) params = %{notification_settings: notification_settings} From 45e4642a58f5299d2cd3f142aea110a474eb477f Mon Sep 17 00:00:00 2001 From: William Pitcock Date: Sun, 26 May 2019 00:20:54 +0000 Subject: [PATCH 29/52] tests: chase remote/local removal --- test/notification_test.exs | 27 ------------------- test/web/mastodon_api/account_view_test.exs | 2 -- test/web/twitter_api/util_controller_test.exs | 3 --- 3 files changed, 32 deletions(-) diff --git a/test/notification_test.exs b/test/notification_test.exs index b54414dcd..be292abd9 100644 --- a/test/notification_test.exs +++ b/test/notification_test.exs @@ -78,33 +78,6 @@ test "it doesn't create a notification for an activity from a muted thread" do assert nil == Notification.create_notification(activity, muter) end - test "it disables notifications from people on remote instances" do - user = insert(:user, info: %{notification_settings: %{"remote" => false}}) - other_user = insert(:user) - - create_activity = %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "type" => "Create", - "to" => ["https://www.w3.org/ns/activitystreams#Public"], - "actor" => other_user.ap_id, - "object" => %{ - "type" => "Note", - "content" => "Hi @#{user.nickname}", - "attributedTo" => other_user.ap_id - } - } - - {:ok, %{local: false} = activity} = Transmogrifier.handle_incoming(create_activity) - assert nil == Notification.create_notification(activity, user) - end - - test "it disables notifications from people on the local instance" do - user = insert(:user, info: %{notification_settings: %{"local" => false}}) - other_user = insert(:user) - {:ok, activity} = CommonAPI.post(other_user, %{"status" => "hey @#{user.nickname}"}) - assert nil == Notification.create_notification(activity, user) - end - test "it disables notifications from followers" do follower = insert(:user) followed = insert(:user, info: %{notification_settings: %{"followers" => false}}) diff --git a/test/web/mastodon_api/account_view_test.exs b/test/web/mastodon_api/account_view_test.exs index 6f8480ee2..23f250990 100644 --- a/test/web/mastodon_api/account_view_test.exs +++ b/test/web/mastodon_api/account_view_test.exs @@ -78,8 +78,6 @@ test "Represent the user account for the account owner" do user = insert(:user) notification_settings = %{ - "remote" => true, - "local" => true, "followers" => true, "follows" => true, "non_follows" => true, diff --git a/test/web/twitter_api/util_controller_test.exs b/test/web/twitter_api/util_controller_test.exs index ca0b8cc26..cab9e5d90 100644 --- a/test/web/twitter_api/util_controller_test.exs +++ b/test/web/twitter_api/util_controller_test.exs @@ -102,7 +102,6 @@ test "it updates notification settings", %{conn: conn} do conn |> assign(:user, user) |> put("/api/pleroma/notification_settings", %{ - "remote" => false, "followers" => false, "bar" => 1 }) @@ -111,8 +110,6 @@ test "it updates notification settings", %{conn: conn} do user = Repo.get(User, user.id) assert %{ - "remote" => false, - "local" => true, "followers" => false, "follows" => true, "non_follows" => true, From 4e45f7bc878638b37225aadcdee547a11d280963 Mon Sep 17 00:00:00 2001 From: lain Date: Fri, 31 May 2019 10:55:35 +0200 Subject: [PATCH 30/52] CI: Add auto-deployment via dokku. --- .buildpacks | 1 + .gitlab-ci.yml | 47 +++++++++++++++++++++++++++++++++++++++++ Procfile | 2 ++ config/dokku.exs | 25 ++++++++++++++++++++++ elixir_buildpack.config | 2 ++ 5 files changed, 77 insertions(+) create mode 100644 .buildpacks create mode 100644 Procfile create mode 100644 config/dokku.exs create mode 100644 elixir_buildpack.config diff --git a/.buildpacks b/.buildpacks new file mode 100644 index 000000000..31dd57096 --- /dev/null +++ b/.buildpacks @@ -0,0 +1 @@ +https://github.com/hashnuke/heroku-buildpack-elixir diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8b5131dc3..53f05ae92 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -95,3 +95,50 @@ docs-deploy: - eval $(ssh-agent -s) - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - - rsync -hrvz --delete -e "ssh -p ${SSH_PORT}" priv/static/doc/ "${SSH_USER_HOST_LOCATION}/${CI_COMMIT_REF_NAME}" + +review_app: + image: alpine:3.9 + stage: deploy + before_script: + - apk update && apk add openssh-client git + when: manual + environment: + name: review/$CI_COMMIT_REF_NAME + url: https://$CI_ENVIRONMENT_SLUG.pleroma.online/ + on_stop: stop_review_app + only: + - branches + except: + - master + - develop + script: + - echo "$CI_ENVIRONMENT_SLUG" + - mkdir -p ~/.ssh + - eval $(ssh-agent -s) + - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - + - ssh-keyscan -H "pleroma.online" >> ~/.ssh/known_hosts + - (ssh -t dokku@pleroma.online -- apps:create "$CI_ENVIRONMENT_SLUG") || true + - ssh -t dokku@pleroma.online -- config:set "$CI_ENVIRONMENT_SLUG" APP_NAME="$CI_ENVIRONMENT_SLUG" APP_HOST="$CI_ENVIRONMENT_SLUG.pleroma.online" MIX_ENV=dokku + - (ssh -t dokku@pleroma.online -- postgres:create $(echo $CI_ENVIRONMENT_SLUG | sed -e 's/-/_/g')_db) || true + - (ssh -t dokku@pleroma.online -- postgres:link $(echo $CI_ENVIRONMENT_SLUG | sed -e 's/-/_/g')_db "$CI_ENVIRONMENT_SLUG") || true + - (ssh -t dokku@pleroma.online -- certs:add "$CI_ENVIRONMENT_SLUG" /home/dokku/server.crt /home/dokku/server.key) || true + - (git remote add dokku dokku@pleroma.online:$CI_ENVIRONMENT_SLUG) || true + - git push -f dokku $CI_COMMIT_SHA:refs/heads/master + +stop_review_app: + image: alpine:3.9 + stage: deploy + before_script: + - apk update && apk add openssh-client git + when: manual + environment: + name: review/$CI_COMMIT_REF_NAME + action: stop + script: + - echo "$CI_ENVIRONMENT_SLUG" + - mkdir -p ~/.ssh + - eval $(ssh-agent -s) + - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - + - ssh-keyscan -H "pleroma.online" >> ~/.ssh/known_hosts + - ssh -t dokku@pleroma.online -- --force apps:destroy "$CI_ENVIRONMENT_SLUG" + - ssh -t dokku@pleroma.online -- --force postgres:destroy $(echo $CI_ENVIRONMENT_SLUG | sed -e 's/-/_/g')_db diff --git a/Procfile b/Procfile new file mode 100644 index 000000000..7ac187baa --- /dev/null +++ b/Procfile @@ -0,0 +1,2 @@ +web: mix phx.server +release: mix ecto.migrate diff --git a/config/dokku.exs b/config/dokku.exs new file mode 100644 index 000000000..9ea0ec450 --- /dev/null +++ b/config/dokku.exs @@ -0,0 +1,25 @@ +use Mix.Config + +config :pleroma, Pleroma.Web.Endpoint, + http: [ + port: String.to_integer(System.get_env("PORT") || "4000"), + protocol_options: [max_request_line_length: 8192, max_header_value_length: 8192] + ], + protocol: "http", + secure_cookie_flag: false, + url: [host: System.get_env("APP_HOST"), scheme: "https", port: 443], + secret_key_base: "+S+ULgf7+N37c/lc9K66SMphnjQIRGklTu0BRr2vLm2ZzvK0Z6OH/PE77wlUNtvP" + +database_url = + System.get_env("DATABASE_URL") || + raise """ + environment variable DATABASE_URL is missing. + For example: ecto://USER:PASS@HOST/DATABASE + """ + +config :pleroma, Pleroma.Repo, + # ssl: true, + url: database_url, + pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10") + +config :pleroma, :instance, name: "#{System.get_env("APP_NAME")} CI Instance" diff --git a/elixir_buildpack.config b/elixir_buildpack.config new file mode 100644 index 000000000..c23b08fb8 --- /dev/null +++ b/elixir_buildpack.config @@ -0,0 +1,2 @@ +elixir_version=1.8.2 +erlang_version=21.3.7 From 97fb50d9faaea509e3e7809689f4aa045634fa81 Mon Sep 17 00:00:00 2001 From: lain Date: Fri, 31 May 2019 11:27:14 +0200 Subject: [PATCH 31/52] Mastodon Conversation API: Don't return own account in 'accounts'. --- lib/pleroma/web/mastodon_api/views/conversation_view.ex | 7 ++++++- test/web/mastodon_api/mastodon_api_controller_test.exs | 6 +++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/mastodon_api/views/conversation_view.ex b/lib/pleroma/web/mastodon_api/views/conversation_view.ex index 8e8f7cf31..af1dcf66d 100644 --- a/lib/pleroma/web/mastodon_api/views/conversation_view.ex +++ b/lib/pleroma/web/mastodon_api/views/conversation_view.ex @@ -22,9 +22,14 @@ def render("participation.json", %{participation: participation, user: user}) do last_status = StatusView.render("status.json", %{activity: activity, for: user}) + # Conversations return all users except the current user. + users = + participation.conversation.users + |> Enum.reject(&(&1.id == user.id)) + accounts = AccountView.render("accounts.json", %{ - users: participation.conversation.users, + users: users, as: :user }) diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs index 1d9f5a816..24cc911c3 100644 --- a/test/web/mastodon_api/mastodon_api_controller_test.exs +++ b/test/web/mastodon_api/mastodon_api_controller_test.exs @@ -317,12 +317,13 @@ test "direct timeline", %{conn: conn} do test "Conversations", %{conn: conn} do user_one = insert(:user) user_two = insert(:user) + user_three = insert(:user) {:ok, user_two} = User.follow(user_two, user_one) {:ok, direct} = CommonAPI.post(user_one, %{ - "status" => "Hi @#{user_two.nickname}!", + "status" => "Hi @#{user_two.nickname}, @#{user_three.nickname}!", "visibility" => "direct" }) @@ -348,7 +349,10 @@ test "Conversations", %{conn: conn} do } ] = response + account_ids = Enum.map(res_accounts, & &1["id"]) assert length(res_accounts) == 2 + assert user_two.id in account_ids + assert user_three.id in account_ids assert is_binary(res_id) assert unread == true assert res_last_status["id"] == direct.id From 6872c5f88f211dd40ff605268f1a0a22acc199c3 Mon Sep 17 00:00:00 2001 From: feld Date: Fri, 31 May 2019 18:46:57 +0000 Subject: [PATCH 32/52] Mastodon API search default value for "resolve" is false https://docs.joinmastodon.org/api/rest/accounts/ https://docs.joinmastodon.org/api/rest/search/ --- lib/pleroma/user.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 5c91dc7d4..474cd8c1a 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -758,7 +758,7 @@ def search_query(query, for_user) do from(s in subquery(boost_search_rank_query(distinct_query, for_user)), order_by: [desc: s.search_rank], - limit: 20 + limit: 40 ) end From abedcca349bf5eefd75fb1e15d5b6ca54401fb27 Mon Sep 17 00:00:00 2001 From: feld Date: Fri, 31 May 2019 20:36:33 +0000 Subject: [PATCH 33/52] Sample Mongoose IM config. Change domain, ensure paths to PEM files are correct, and this is good to go. --- installation/pleroma-mongooseim.cfg | 932 ++++++++++++++++++++++++++++ 1 file changed, 932 insertions(+) create mode 100755 installation/pleroma-mongooseim.cfg diff --git a/installation/pleroma-mongooseim.cfg b/installation/pleroma-mongooseim.cfg new file mode 100755 index 000000000..d7567321f --- /dev/null +++ b/installation/pleroma-mongooseim.cfg @@ -0,0 +1,932 @@ +%%% +%%% ejabberd configuration file +%%% +%%%' + +%%% The parameters used in this configuration file are explained in more detail +%%% in the ejabberd Installation and Operation Guide. +%%% Please consult the Guide in case of doubts, it is included with +%%% your copy of ejabberd, and is also available online at +%%% http://www.process-one.net/en/ejabberd/docs/ + +%%% This configuration file contains Erlang terms. +%%% In case you want to understand the syntax, here are the concepts: +%%% +%%% - The character to comment a line is % +%%% +%%% - Each term ends in a dot, for example: +%%% override_global. +%%% +%%% - A tuple has a fixed definition, its elements are +%%% enclosed in {}, and separated with commas: +%%% {loglevel, 4}. +%%% +%%% - A list can have as many elements as you want, +%%% and is enclosed in [], for example: +%%% [http_poll, web_admin, tls] +%%% +%%% Pay attention that list elements are delimited with commas, +%%% but no comma is allowed after the last list element. This will +%%% give a syntax error unlike in more lenient languages (e.g. Python). +%%% +%%% - A keyword of ejabberd is a word in lowercase. +%%% Strings are enclosed in "" and can contain spaces, dots, ... +%%% {language, "en"}. +%%% {ldap_rootdn, "dc=example,dc=com"}. +%%% +%%% - This term includes a tuple, a keyword, a list, and two strings: +%%% {hosts, ["jabber.example.net", "im.example.com"]}. +%%% +%%% - This config is preprocessed during release generation by a tool which +%%% interprets double curly braces as substitution markers, so avoid this +%%% syntax in this file (though it's valid Erlang). +%%% +%%% So this is OK (though arguably looks quite ugly): +%%% { {s2s_addr, "example-host.net"}, {127,0,0,1} }. +%%% +%%% And I can't give an example of what's not OK exactly because +%%% of this rule. +%%% + + +%%%. ======================= +%%%' OVERRIDE STORED OPTIONS + +%% +%% Override the old values stored in the database. +%% + +%% +%% Override global options (shared by all ejabberd nodes in a cluster). +%% +%%override_global. + +%% +%% Override local options (specific for this particular ejabberd node). +%% +%%override_local. + +%% +%% Remove the Access Control Lists before new ones are added. +%% +%%override_acls. + + +%%%. ========= +%%%' DEBUGGING + +%% +%% loglevel: Verbosity of log files generated by ejabberd. +%% 0: No ejabberd log at all (not recommended) +%% 1: Critical +%% 2: Error +%% 3: Warning +%% 4: Info +%% 5: Debug +%% +{loglevel, 3}. + +%%%. ================ +%%%' SERVED HOSTNAMES + +%% +%% hosts: Domains served by ejabberd. +%% You can define one or several, for example: +%% {hosts, ["example.net", "example.com", "example.org"]}. +%% +{hosts, ["pleroma.soykaf.com"] }. + +%% +%% route_subdomains: Delegate subdomains to other XMPP servers. +%% For example, if this ejabberd serves example.org and you want +%% to allow communication with an XMPP server called im.example.org. +%% +%%{route_subdomains, s2s}. + + +%%%. =============== +%%%' LISTENING PORTS + +%% +%% listen: The ports ejabberd will listen on, which service each is handled +%% by and what options to start it with. +%% +{listen, + [ + %% BOSH and WS endpoints over HTTP + { 5280, ejabberd_cowboy, [ + {num_acceptors, 10}, + {transport_options, [{max_connections, 1024}]}, + {modules, [ + + {"_", "/http-bind", mod_bosh}, + {"_", "/ws-xmpp", mod_websockets, [{ejabberd_service, [ + {access, all}, + {shaper_rule, fast}, + {ip, {127, 0, 0, 1}}, + {password, "secret"}]} + %% Uncomment to enable connection dropping or/and server-side pings + %{timeout, 600000}, {ping_rate, 2000} + ]} + %% Uncomment to serve static files + %{"_", "/static/[...]", cowboy_static, + % {dir, "/var/www", [{mimetypes, cow_mimetypes, all}]} + %}, + + %% Example usage of mod_revproxy + + %% {"_", "/[...]", mod_revproxy, [{timeout, 5000}, + %% % time limit for upstream to respond + %% {body_length, 8000000}, + %% % maximum body size (may be infinity) + %% {custom_headers, [{<<"header">>,<<"value">>}]} + %% % list of extra headers that are send to upstream + %% ]} + + %% Example usage of mod_cowboy + + %% {"_", "/[...]", mod_cowboy, [{http, mod_revproxy, + %% [{timeout, 5000}, + %% % time limit for upstream to respond + %% {body_length, 8000000}, + %% % maximum body size (may be infinity) + %% {custom_headers, [{<<"header">>,<<"value">>}]} + %% % list of extra headers that are send to upstream + %% ]}, + %% {ws, xmpp, mod_websockets} + %% ]} + ]} + ]}, + + %% BOSH and WS endpoints over HTTPS + { 5285, ejabberd_cowboy, [ + {num_acceptors, 10}, + {transport_options, [{max_connections, 1024}]}, + {ssl, [{certfile, "priv/ssl/fullchain.pem"}, {keyfile, "priv/ssl/privkey.pem"}, {password, ""}]}, + {modules, [ + {"_", "/http-bind", mod_bosh}, + {"_", "/ws-xmpp", mod_websockets, [ + %% Uncomment to enable connection dropping or/and server-side pings + %{timeout, 600000}, {ping_rate, 60000} + ]} + %% Uncomment to serve static files + %{"_", "/static/[...]", cowboy_static, + % {dir, "/var/www", [{mimetypes, cow_mimetypes, all}]} + %}, + ]} + ]}, + + %% MongooseIM HTTP API it's important to start it on localhost + %% or some private interface only (not accessible from the outside) + %% At least start it on different port which will be hidden behind firewall + + { {8088, "127.0.0.1"} , ejabberd_cowboy, [ + {num_acceptors, 10}, + {transport_options, [{max_connections, 1024}]}, + {modules, [ + {"localhost", "/api", mongoose_api_admin, []} + ]} + ]}, + + { 8089 , ejabberd_cowboy, [ + {num_acceptors, 10}, + {transport_options, [{max_connections, 1024}]}, + {protocol_options, [{compress, true}]}, + {ssl, [{certfile, "priv/ssl/fullchain.pem"}, {keyfile, "priv/ssl/privkey.pem"}, {password, ""}]}, + {modules, [ + {"_", "/api/sse", lasse_handler, [mongoose_client_api_sse]}, + {"_", "/api/messages/[:with]", mongoose_client_api_messages, []}, + {"_", "/api/contacts/[:jid]", mongoose_client_api_contacts, []}, + {"_", "/api/rooms/[:id]", mongoose_client_api_rooms, []}, + {"_", "/api/rooms/[:id]/config", mongoose_client_api_rooms_config, []}, + {"_", "/api/rooms/:id/users/[:user]", mongoose_client_api_rooms_users, []}, + {"_", "/api/rooms/[:id]/messages", mongoose_client_api_rooms_messages, []} + ]} + ]}, + + %% Following HTTP API is deprected, the new one abouve should be used instead + + { {5288, "127.0.0.1"} , ejabberd_cowboy, [ + {num_acceptors, 10}, + {transport_options, [{max_connections, 1024}]}, + {modules, [ + {"localhost", "/api", mongoose_api, [{handlers, [mongoose_api_metrics, + mongoose_api_users]}]} + ]} + ]}, + + { 5222, ejabberd_c2s, [ + + %% + %% If TLS is compiled in and you installed a SSL + %% certificate, specify the full path to the + %% file and uncomment this line: + %% + {certfile, "priv/ssl/both.pem"}, starttls, + + %%{zlib, 10000}, + %% https://www.openssl.org/docs/apps/ciphers.html#CIPHER_STRINGS + %% {ciphers, "DEFAULT:!EXPORT:!LOW:!SSLv2"}, + {access, c2s}, + {shaper, c2s_shaper}, + {max_stanza_size, 65536}, + {protocol_options, ["no_sslv3"]} + + ]}, + + + + %% + %% To enable the old SSL connection method on port 5223: + %% + %%{5223, ejabberd_c2s, [ + %% {access, c2s}, + %% {shaper, c2s_shaper}, + %% {certfile, "/path/to/ssl.pem"}, tls, + %% {max_stanza_size, 65536} + %% ]}, + + { 5269, ejabberd_s2s_in, [ + {shaper, s2s_shaper}, + {max_stanza_size, 131072}, + {protocol_options, ["no_sslv3"]} + + ]} + + %% + %% ejabberd_service: Interact with external components (transports, ...) + %% + ,{8888, ejabberd_service, [ + {access, all}, + {shaper_rule, fast}, + {ip, {127, 0, 0, 1}}, + {password, "secret"} + ]} + + %% + %% ejabberd_stun: Handles STUN Binding requests + %% + %%{ {3478, udp}, ejabberd_stun, []} + + ]}. + +%% +%% s2s_use_starttls: Enable STARTTLS + Dialback for S2S connections. +%% Allowed values are: false optional required required_trusted +%% You must specify a certificate file. +%% +{s2s_use_starttls, optional}. +%% +%% s2s_certfile: Specify a certificate file. +%% +{s2s_certfile, "priv/ssl/both.pem"}. + +%% https://www.openssl.org/docs/apps/ciphers.html#CIPHER_STRINGS +%% {s2s_ciphers, "DEFAULT:!EXPORT:!LOW:!SSLv2"}. + +%% +%% domain_certfile: Specify a different certificate for each served hostname. +%% +%%{domain_certfile, "example.org", "/path/to/example_org.pem"}. +%%{domain_certfile, "example.com", "/path/to/example_com.pem"}. + +%% +%% S2S whitelist or blacklist +%% +%% Default s2s policy for undefined hosts. +%% +{s2s_default_policy, deny }. + +%% +%% Allow or deny communication with specific servers. +%% +%%{ {s2s_host, "goodhost.org"}, allow}. +%%{ {s2s_host, "badhost.org"}, deny}. + +{outgoing_s2s_port, 5269 }. + +%% +%% IP addresses predefined for specific hosts to skip DNS lookups. +%% Ports defined here take precedence over outgoing_s2s_port. +%% Examples: +%% +%% { {s2s_addr, "example-host.net"}, {127,0,0,1} }. +%% { {s2s_addr, "example-host.net"}, { {127,0,0,1}, 5269 } }. +%% { {s2s_addr, "example-host.net"}, { {127,0,0,1}, 5269 } }. + +%% +%% Outgoing S2S options +%% +%% Preferred address families (which to try first) and connect timeout +%% in milliseconds. +%% +%%{outgoing_s2s_options, [ipv4, ipv6], 10000}. +%% +%%%. ============== +%%%' SESSION BACKEND + +%%{sm_backend, {mnesia, []}}. + +%% Requires {redis, global, default, ..., ...} outgoing pool +%%{sm_backend, {redis, []}}. + +{sm_backend, {mnesia, []} }. + + +%%%. ============== +%%%' AUTHENTICATION + +%% Advertised SASL mechanisms +{sasl_mechanisms, [cyrsasl_plain]}. + +%% +%% auth_method: Method used to authenticate the users. +%% The default method is the internal. +%% If you want to use a different method, +%% comment this line and enable the correct ones. +%% +%% {auth_method, internal }. +{auth_method, http }. +{auth_opts, [ + {http, global, auth, [{workers, 50}], [{server, "https://pleroma.soykaf.com"}]}, + {password_format, plain} % default + %% {password_format, scram} + + %% {scram_iterations, 4096} % default + + %% + %% For auth_http: + %% {basic_auth, "user:password"} + %% {path_prefix, "/"} % default + %% auth_http requires {http, Host | global, auth, ..., ...} outgoing pool. + %% + %% For auth_external + %%{extauth_program, "/path/to/authentication/script"}. + %% + %% For auth_jwt + %% {jwt_secret_source, "/path/to/file"}, + %% {jwt_algorithm, "RS256"}, + %% {jwt_username_key, user} + %% For cyrsasl_external + %% {authenticate_with_cn, false} + {cyrsasl_external, standard} + ]}. + +%% +%% Authentication using external script +%% Make sure the script is executable by ejabberd. +%% +%%{auth_method, external}. + +%% +%% Authentication using RDBMS +%% Remember to setup a database in the next section. +%% +%%{auth_method, rdbms}. + +%% +%% Authentication using LDAP +%% +%%{auth_method, ldap}. +%% + +%% List of LDAP servers: +%%{ldap_servers, ["localhost"]}. +%% +%% Encryption of connection to LDAP servers: +%%{ldap_encrypt, none}. +%%{ldap_encrypt, tls}. +%% +%% Port to connect to on LDAP servers: +%%{ldap_port, 389}. +%%{ldap_port, 636}. +%% +%% LDAP manager: +%%{ldap_rootdn, "dc=example,dc=com"}. +%% +%% Password of LDAP manager: +%%{ldap_password, "******"}. +%% +%% Search base of LDAP directory: +%%{ldap_base, "dc=example,dc=com"}. +%% +%% LDAP attribute that holds user ID: +%%{ldap_uids, [{"mail", "%u@mail.example.org"}]}. +%% +%% LDAP filter: +%%{ldap_filter, "(objectClass=shadowAccount)"}. + +%% +%% Anonymous login support: +%% auth_method: anonymous +%% anonymous_protocol: sasl_anon | login_anon | both +%% allow_multiple_connections: true | false +%% +%%{host_config, "public.example.org", [{auth_method, anonymous}, +%% {allow_multiple_connections, false}, +%% {anonymous_protocol, sasl_anon}]}. +%% +%% To use both anonymous and internal authentication: +%% +%%{host_config, "public.example.org", [{auth_method, [internal, anonymous]}]}. + + +%%%. ============== +%%%' OUTGOING CONNECTIONS (e.g. DB) + +%% Here you may configure all outgoing connections used by MongooseIM, +%% e.g. to RDBMS (such as MySQL), Riak or external HTTP components. +%% Default MongooseIM configuration uses only Mnesia (non-Mnesia extensions are disabled), +%% so no options here are uncommented out of the box. +%% This section includes configuration examples; for comprehensive guide +%% please consult MongooseIM documentation, page "Outgoing connections": +%% - doc/advanced-configuration/outgoing-connections.md +%% - https://mongooseim.readthedocs.io/en/latest/advanced-configuration/outgoing-connections/ + + +{outgoing_pools, [ +% {riak, global, default, [{workers, 5}], [{address, "127.0.0.1"}, {port, 8087}]}, +% {elastic, global, default, [], [{host, "elastic.host.com"}, {port, 9042}]}, + {http, global, auth, [{workers, 50}], [{server, "https://pleroma.soykaf.com"}]} +% {cassandra, global, default, [{workers, 100}], [{servers, [{"server1", 9042}]}, {keyspace, "big_mongooseim"}]}, +% {rdbms, global, default, [{workers, 10}], [{server, {mysql, "server", 3306, "database", "username", "password"}}]} +]}. + +%% More examples that may be added to outgoing_pools list: +%% +%% == MySQL == +%% {rdbms, global, default, [{workers, 10}], +%% [{server, {mysql, "server", 3306, "database", "username", "password"}}, +%% {keepalive_interval, 10}]}, +%% keepalive_interval is optional + +%% == PostgreSQL == +%% {rdbms, global, default, [{workers, 10}], +%% [{server, {pgsql, "server", 5432, "database", "username", "password"}}]}, + +%% == ODBC (MSSQL) == +%% {rdbms, global, default, [{workers, 10}], +%% [{server, "DSN=mongooseim;UID=mongooseim;PWD=mongooseim"}]}, + +%% == Elastic Search == +%% {elastic, global, default, [], [{host, "elastic.host.com"}, {port, 9042}]}, + +%% == Riak == +%% {riak, global, default, [{workers, 20}], [{address, "127.0.0.1"}, {port, 8087}]}, + +%% == HTTP == +%% {http, global, conn1, [{workers, 50}], [{server, "http://server:8080"}]}, + +%% == Cassandra == +%% {cassandra, global, default, [{workers, 100}], +%% [ +%% {servers, [ +%% {"cassandra_server1.example.com", 9042}, +%% {"cassandra_server2.example.com", 9042}, +%% {"cassandra_server3.example.com", 9042}, +%% {"cassandra_server4.example.com", 9042} +%% ]}, +%% {keyspace, "big_mongooseim"} +%% ]} + +%% == Extra options == +%% +%% If you use PostgreSQL, have a large database, and need a +%% faster but inexact replacement for "select count(*) from users" +%% +%%{pgsql_users_number_estimate, true}. +%% +%% rdbms_server_type specifies what database is used over the RDBMS layer +%% Can take values mssql, pgsql, mysql +%% In some cases (for example for MAM with pgsql) it is required to set proper value. +%% +%% {rdbms_server_type, pgsql}. + +%%%. =============== +%%%' TRAFFIC SHAPERS + +%% +%% The "normal" shaper limits traffic speed to 1000 B/s +%% +{shaper, normal, {maxrate, 1000}}. + +%% +%% The "fast" shaper limits traffic speed to 50000 B/s +%% +{shaper, fast, {maxrate, 50000}}. + +%% +%% This option specifies the maximum number of elements in the queue +%% of the FSM. Refer to the documentation for details. +%% +{max_fsm_queue, 1000}. + +%%%. ==================== +%%%' ACCESS CONTROL LISTS + +%% +%% The 'admin' ACL grants administrative privileges to XMPP accounts. +%% You can put here as many accounts as you want. +%% +%{acl, admin, {user, "alice", "localhost"}}. +%{acl, admin, {user, "a", "localhost"}}. + +%% +%% Blocked users +%% +%%{acl, blocked, {user, "baduser", "example.org"}}. +%%{acl, blocked, {user, "test"}}. + +%% +%% Local users: don't modify this line. +%% +{acl, local, {user_regexp, ""}}. + +%% +%% More examples of ACLs +%% +%%{acl, jabberorg, {server, "jabber.org"}}. +%%{acl, aleksey, {user, "aleksey", "jabber.ru"}}. +%%{acl, test, {user_regexp, "^test"}}. +%%{acl, test, {user_glob, "test*"}}. + +%% +%% Define specific ACLs in a virtual host. +%% +%%{host_config, "localhost", +%% [ +%% {acl, admin, {user, "bob-local", "localhost"}} +%% ] +%%}. + +%%%. ============ +%%%' ACCESS RULES + +%% Maximum number of simultaneous sessions allowed for a single user: +{access, max_user_sessions, [{10, all}]}. + +%% Maximum number of offline messages that users can have: +{access, max_user_offline_messages, [{5000, admin}, {100, all}]}. + +%% This rule allows access only for local users: +{access, local, [{allow, local}]}. + +%% Only non-blocked users can use c2s connections: +{access, c2s, [{deny, blocked}, + {allow, all}]}. + +%% For C2S connections, all users except admins use the "normal" shaper +{access, c2s_shaper, [{none, admin}, + {normal, all}]}. + +%% All S2S connections use the "fast" shaper +{access, s2s_shaper, [{fast, all}]}. + +%% Admins of this server are also admins of the MUC service: +{access, muc_admin, [{allow, admin}]}. + +%% Only accounts of the local ejabberd server can create rooms: +{access, muc_create, [{allow, local}]}. + +%% All users are allowed to use the MUC service: +{access, muc, [{allow, all}]}. + +%% In-band registration allows registration of any possible username. +%% To disable in-band registration, replace 'allow' with 'deny'. +{access, register, [{allow, all}]}. + +%% By default the frequency of account registrations from the same IP +%% is limited to 1 account every 10 minutes. To disable, specify: infinity +{registration_timeout, infinity}. + +%% Default settings for MAM. +%% To set non-standard value, replace 'default' with 'allow' or 'deny'. +%% Only user can access his/her archive by default. +%% An online user can read room's archive by default. +%% Only an owner can change settings and purge messages by default. +%% Empty list (i.e. `[]`) means `[{deny, all}]`. +{access, mam_set_prefs, [{default, all}]}. +{access, mam_get_prefs, [{default, all}]}. +{access, mam_lookup_messages, [{default, all}]}. +{access, mam_purge_single_message, [{default, all}]}. +{access, mam_purge_multiple_messages, [{default, all}]}. + +%% 1 command of the specified type per second. +{shaper, mam_shaper, {maxrate, 1}}. +%% This shaper is primeraly for Mnesia overload protection during stress testing. +%% The limit is 1000 operations of each type per second. +{shaper, mam_global_shaper, {maxrate, 1000}}. + +{access, mam_set_prefs_shaper, [{mam_shaper, all}]}. +{access, mam_get_prefs_shaper, [{mam_shaper, all}]}. +{access, mam_lookup_messages_shaper, [{mam_shaper, all}]}. +{access, mam_purge_single_message_shaper, [{mam_shaper, all}]}. +{access, mam_purge_multiple_messages_shaper, [{mam_shaper, all}]}. + +{access, mam_set_prefs_global_shaper, [{mam_global_shaper, all}]}. +{access, mam_get_prefs_global_shaper, [{mam_global_shaper, all}]}. +{access, mam_lookup_messages_global_shaper, [{mam_global_shaper, all}]}. +{access, mam_purge_single_message_global_shaper, [{mam_global_shaper, all}]}. +{access, mam_purge_multiple_messages_global_shaper, [{mam_global_shaper, all}]}. + +%% +%% Define specific Access Rules in a virtual host. +%% +%%{host_config, "localhost", +%% [ +%% {access, c2s, [{allow, admin}, {deny, all}]}, +%% {access, register, [{deny, all}]} +%% ] +%%}. + +%%%. ================ +%%%' DEFAULT LANGUAGE + +%% +%% language: Default language used for server messages. +%% +{language, "en"}. + +%% +%% Set a different default language in a virtual host. +%% +%%{host_config, "localhost", +%% [{language, "ru"}] +%%}. + +%%%. ================ +%%%' MISCELLANEOUS + +{all_metrics_are_global, false }. + +%%%. ======== +%%%' SERVICES + +%% Unlike modules, services are started per node and provide either features which are not +%% related to any particular host, or backend stuff which is used by modules. +%% This is handled by `mongoose_service` module. + +{services, + [ + {service_admin_extra, [{submods, [node, accounts, sessions, vcard, + roster, last, private, stanza, stats]}]} + ] +}. + +%%%. ======= +%%%' MODULES + +%% +%% Modules enabled in all mongooseim virtual hosts. +%% For list of possible modules options, check documentation. +%% +{modules, + [ + + %% The format for a single route is as follows: + %% {Host, Path, Method, Upstream} + %% + %% "_" can be used as wildcard for Host, Path and Method + %% Upstream can be either host (just http(s)://host:port) or uri + %% The difference is that host upstreams append whole path while + %% uri upstreams append only remainder that follows the matched Path + %% (this behaviour is similar to nginx's proxy_pass rules) + %% + %% Bindings can be used to match certain parts of host or path. + %% They will be later overlaid with parts of the upstream uri. + %% + %% {mod_revproxy, + %% [{routes, [{"www.erlang-solutions.com", "/admin", "_", + %% "https://www.erlang-solutions.com/"}, + %% {":var.com", "/:var", "_", "http://localhost:8080/"}, + %% {":domain.com", "/", "_", "http://localhost:8080/:domain"}] + %% }]}, + +% {mod_http_upload, [ + %% Set max file size in bytes. Defaults to 10 MB. + %% Disabled if value is `undefined`. +% {max_file_size, 1024}, + %% Use S3 storage backend +% {backend, s3}, + %% Set options for S3 backend +% {s3, [ +% {bucket_url, "http://s3-eu-west-1.amazonaws.com/konbucket2"}, +% {region, "eu-west-1"}, +% {access_key_id, "AKIAIAOAONIULXQGMOUA"}, +% {secret_access_key, "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"} +% ]} +% ]}, + + {mod_adhoc, []}, + + {mod_disco, [{users_can_see_hidden_services, false}]}, + {mod_commands, []}, + {mod_muc_commands, []}, + {mod_muc_light_commands, []}, + {mod_last, []}, + {mod_stream_management, [ + % default 100 + % size of a buffer of unacked messages + % {buffer_max, 100} + + % default 1 - server sends the ack request after each stanza + % {ack_freq, 1} + + % default: 600 seconds + % {resume_timeout, 600} + ]}, + %% {mod_muc_light, [{host, "muclight.@HOST@"}]}, + %% {mod_muc, [{host, "muc.@HOST@"}, + %% {access, muc}, + %% {access_create, muc_create} + %% ]}, + %% {mod_muc_log, [ + %% {outdir, "/tmp/muclogs"}, + %% {access_log, muc} + %% ]}, + {mod_offline, [{access_max_user_messages, max_user_offline_messages}]}, + {mod_privacy, []}, + {mod_blocking, []}, + {mod_private, []}, +% {mod_private, [{backend, mnesia}]}, +% {mod_private, [{backend, rdbms}]}, +% {mod_register, [ +% %% +% %% Set the minimum informational entropy for passwords. +% %% +% %%{password_strength, 32}, +% +% %% +% %% After successful registration, the user receives +% %% a message with this subject and body. +% %% +% {welcome_message, {""}}, +% +% %% +% %% When a user registers, send a notification to +% %% these XMPP accounts. +% %% +% +% +% %% +% %% Only clients in the server machine can register accounts +% %% +% {ip_access, [{allow, "127.0.0.0/8"}, +% {deny, "0.0.0.0/0"}]}, +% +% %% +% %% Local c2s or remote s2s users cannot register accounts +% %% +% %%{access_from, deny}, +% +% {access, register} +% ]}, + {mod_roster, []}, + {mod_sic, []}, + {mod_vcard, [%{matches, 1}, +%{search, true}, +%{ldap_search_operator, 'or'}, %% either 'or' or 'and' +%{ldap_binary_search_fields, [<<"PHOTO">>]}, +%% list of binary search fields (as in vcard after mapping) +{host, "vjud.@HOST@"} +]}, + {mod_bosh, []}, + {mod_carboncopy, []} + + %% + %% Message Archive Management (MAM, XEP-0313) for registered users and + %% Multi-User chats (MUCs). + %% + +% {mod_mam_meta, [ + %% Use RDBMS backend (default) +% {backend, rdbms}, + + %% Do not store user preferences (default) +% {user_prefs_store, false}, + %% Store user preferences in RDBMS +% {user_prefs_store, rdbms}, + %% Store user preferences in Mnesia (recommended). + %% The preferences store will be called each time, as a message is routed. + %% That is why Mnesia is better suited for this job. +% {user_prefs_store, mnesia}, + + %% Enables a pool of asynchronous writers. (default) + %% Messages will be grouped together based on archive id. +% {async_writer, true}, + + %% Cache information about users (default) +% {cache_users, true}, + + %% Enable archivization for private messages (default) +% {pm, [ + %% Top-level options can be overriden here if needed, for example: +% {async_writer, false} +% ]}, + + %% + %% Message Archive Management (MAM) for multi-user chats (MUC). + %% Enable XEP-0313 for "muc.@HOST@". + %% +% {muc, [ +% {host, "muc.@HOST@"} + %% As with pm, top-level options can be overriden for MUC archive +% ]}, +% + %% Do not use a element (by default stanzaid is used) +% no_stanzaid_element, +% ]}, + + + %% + %% MAM configuration examples + %% + + %% Only MUC, no user-defined preferences, good performance. +% {mod_mam_meta, [ +% {backend, rdbms}, +% {pm, false}, +% {muc, [ +% {host, "muc.@HOST@"} +% ]} +% ]}, + + %% Only archives for c2c messages, good performance. +% {mod_mam_meta, [ +% {backend, rdbms}, +% {pm, [ +% {user_prefs_store, mnesia} +% ]} +% ]}, + + %% Basic configuration for c2c messages, bad performance, easy to debug. +% {mod_mam_meta, [ +% {backend, rdbms}, +% {async_writer, false}, +% {cache_users, false} +% ]}, + + %% Cassandra archive for c2c and MUC conversations. + %% No custom settings supported (always archive). +% {mod_mam_meta, [ +% {backend, cassandra}, +% {user_prefs_store, cassandra}, +% {muc, [{host, "muc.@HOST@"}]} +% ]} + +% {mod_event_pusher, [ +% {backends, [ +% %% +% %% Configuration for Amazon SNS notifications. +% %% +% {sns, [ +% %% AWS credentials, region and host configuration +% {access_key_id, "AKIAJAZYHOIPY6A2PESA"}, +% {secret_access_key, "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"}, +% {region, "eu-west-1"}, +% {account_id, "251423380551"}, +% {region, "eu-west-1"}, +% {sns_host, "sns.eu-west-1.amazonaws.com"}, +% +% %% Messages from this MUC host will be sent to the SNS topic +% {muc_host, "muc.@HOST@"}, +% +% %% Plugin module for defining custom message attributes and user identification +% {plugin_module, mod_event_pusher_sns_defaults}, +% +% %% Topic name configurations. Removing a topic will disable this specific SNS notification +% {presence_updates_topic, "user_presence_updated-dev-1"}, %% For presence updates +% {pm_messages_topic, "user_message_sent-dev-1"}, %% For private chat messages +% {muc_messages_topic, "user_messagegroup_sent-dev-1"} %% For group chat messages +% +% %% Pool options +% {pool_size, 100}, %% Worker pool size for publishing notifications +% {publish_retry_count, 2}, %% Retry count in case of publish error +% {publish_retry_time_ms, 50} %% Base exponential backoff time (in ms) for publish errors +% ]} +% ]} + +]}. + + +%% +%% Enable modules with custom options in a specific virtual host +%% +%%{host_config, "localhost", +%% [{ {add, modules}, +%% [ +%% {mod_some_module, []} +%% ] +%% } +%% ]}. + +%%%. +%%%' + +%%% $Id$ + +%%% Local Variables: +%%% mode: erlang +%%% End: +%%% vim: set filetype=erlang tabstop=8 foldmarker=%%%',%%%. foldmethod=marker: +%%%. From aa9f52edd2ff8fa9dc6008c94717a1399f54dfb8 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Sat, 1 Jun 2019 01:37:11 +0200 Subject: [PATCH 34/52] mastodon_api_controller.ex: Remove vanilla from supported_flavours --- lib/pleroma/web/mastodon_api/mastodon_api_controller.ex | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index 2110027c3..526581363 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -1456,7 +1456,8 @@ def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _para end end - @supported_flavours ["glitch", "vanilla"] + # Note: vanilla is fully unsupported + @supported_flavours ["glitch"] def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params) when flavour in @supported_flavours do From 5d3ece2861b619c101d6860fbc1a80bfd8630ba7 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Sat, 1 Jun 2019 01:42:46 +0200 Subject: [PATCH 35/52] Remove now useless flavours switching --- docs/api/pleroma_api.md | 14 ------- lib/pleroma/user/info.ex | 9 ---- .../mastodon_api/mastodon_api_controller.ex | 42 +------------------ lib/pleroma/web/router.ex | 4 -- .../mastodon_api/mastodon/index.html.eex | 8 ++-- .../mastodon_api_controller_test.exs | 25 ----------- 6 files changed, 5 insertions(+), 97 deletions(-) diff --git a/docs/api/pleroma_api.md b/docs/api/pleroma_api.md index 4d99a2d2b..edc62727a 100644 --- a/docs/api/pleroma_api.md +++ b/docs/api/pleroma_api.md @@ -126,20 +126,6 @@ Request parameters can be passed via [query strings](https://en.wikipedia.org/wi ## `/api/pleroma/admin/`… See [Admin-API](Admin-API.md) -## `/api/v1/pleroma/flavour/:flavour` -* Method `POST` -* Authentication: required -* Response: JSON string. Returns the user flavour or the default one on success, otherwise returns `{"error": "error_msg"}` -* Example response: "glitch" -* Note: This is intended to be used only by mastofe - -## `/api/v1/pleroma/flavour` -* Method `GET` -* Authentication: required -* Response: JSON string. Returns the user flavour or the default one. -* Example response: "glitch" -* Note: This is intended to be used only by mastofe - ## `/api/pleroma/notifications/read` ### Mark a single notification as read * Method `POST` diff --git a/lib/pleroma/user/info.ex b/lib/pleroma/user/info.ex index 6397e2737..ef506c8da 100644 --- a/lib/pleroma/user/info.ex +++ b/lib/pleroma/user/info.ex @@ -42,7 +42,6 @@ defmodule Pleroma.User.Info do field(:hide_follows, :boolean, default: false) field(:hide_favorites, :boolean, default: true) field(:pinned_activities, {:array, :string}, default: []) - field(:flavour, :string, default: nil) field(:mascot, :map, default: nil) field(:emoji, {:array, :map}, default: []) @@ -241,14 +240,6 @@ def mastodon_settings_update(info, settings) do |> validate_required([:settings]) end - def mastodon_flavour_update(info, flavour) do - params = %{flavour: flavour} - - info - |> cast(params, [:flavour]) - |> validate_required([:flavour]) - end - def mascot_update(info, url) do params = %{mascot: url} diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index 526581363..0d81fb840 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -1346,8 +1346,6 @@ def index(%{assigns: %{user: user}} = conn, _params) do accounts = Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user})) - flavour = get_user_flavour(user) - initial_state = %{ meta: %{ @@ -1433,7 +1431,7 @@ def index(%{assigns: %{user: user}} = conn, _params) do conn |> put_layout(false) |> put_view(MastodonView) - |> render("index.html", %{initial_state: initial_state, flavour: flavour}) + |> render("index.html", %{initial_state: initial_state}) else conn |> put_session(:return_to, conn.request_path) @@ -1456,44 +1454,6 @@ def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _para end end - # Note: vanilla is fully unsupported - @supported_flavours ["glitch"] - - def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params) - when flavour in @supported_flavours do - flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour) - - with changeset <- Ecto.Changeset.change(user), - changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng), - {:ok, user} <- User.update_and_set_cache(changeset), - flavour <- user.info.flavour do - json(conn, flavour) - else - e -> - conn - |> put_resp_content_type("application/json") - |> send_resp(500, Jason.encode!(%{"error" => inspect(e)})) - end - end - - def set_flavour(conn, _params) do - conn - |> put_status(400) - |> json(%{error: "Unsupported flavour"}) - end - - def get_flavour(%{assigns: %{user: user}} = conn, _params) do - json(conn, get_user_flavour(user)) - end - - defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do - flavour - end - - defp get_user_flavour(_) do - "glitch" - end - def login(%{assigns: %{user: %User{}}} = conn, _params) do redirect(conn, to: local_mastodon_root_path(conn)) end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 352268b96..dd7c24b52 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -309,8 +309,6 @@ defmodule Pleroma.Web.Router do post("/conversations/:id/read", MastodonAPIController, :conversation_read) get("/endorsements", MastodonAPIController, :empty_array) - - get("/pleroma/flavour", MastodonAPIController, :get_flavour) end scope [] do @@ -350,8 +348,6 @@ defmodule Pleroma.Web.Router do put("/filters/:id", MastodonAPIController, :update_filter) delete("/filters/:id", MastodonAPIController, :delete_filter) - post("/pleroma/flavour/:flavour", MastodonAPIController, :set_flavour) - get("/pleroma/mascot", MastodonAPIController, :get_mascot) put("/pleroma/mascot", MastodonAPIController, :set_mascot) diff --git a/lib/pleroma/web/templates/mastodon_api/mastodon/index.html.eex b/lib/pleroma/web/templates/mastodon_api/mastodon/index.html.eex index ac63811d1..3325beca1 100644 --- a/lib/pleroma/web/templates/mastodon_api/mastodon/index.html.eex +++ b/lib/pleroma/web/templates/mastodon_api/mastodon/index.html.eex @@ -8,7 +8,7 @@ - + @@ -19,10 +19,10 @@ - - + + - +
diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs index 93ef630f2..f5f87d8af 100644 --- a/test/web/mastodon_api/mastodon_api_controller_test.exs +++ b/test/web/mastodon_api/mastodon_api_controller_test.exs @@ -2852,31 +2852,6 @@ test "unmute conversation", %{conn: conn, user: user, activity: activity} do end end - test "flavours switching (Pleroma Extension)", %{conn: conn} do - user = insert(:user) - - get_old_flavour = - conn - |> assign(:user, user) - |> get("/api/v1/pleroma/flavour") - - assert "glitch" == json_response(get_old_flavour, 200) - - set_flavour = - conn - |> assign(:user, user) - |> post("/api/v1/pleroma/flavour/vanilla") - - assert "vanilla" == json_response(set_flavour, 200) - - get_new_flavour = - conn - |> assign(:user, user) - |> post("/api/v1/pleroma/flavour/vanilla") - - assert json_response(set_flavour, 200) == json_response(get_new_flavour, 200) - end - describe "reports" do setup do reporter = insert(:user) From d4304a39459e74b60bc65280a0b0185b3ee6250a Mon Sep 17 00:00:00 2001 From: Sachin Joshi Date: Sat, 1 Jun 2019 08:37:10 +0545 Subject: [PATCH 36/52] add the missing mock file --- test/fixtures/httpoison_mock/emelie.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 test/fixtures/httpoison_mock/emelie.json diff --git a/test/fixtures/httpoison_mock/emelie.json b/test/fixtures/httpoison_mock/emelie.json new file mode 100644 index 000000000..592fc0e4e --- /dev/null +++ b/test/fixtures/httpoison_mock/emelie.json @@ -0,0 +1 @@ +{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1",{"manuallyApprovesFollowers":"as:manuallyApprovesFollowers","toot":"http://joinmastodon.org/ns#","featured":{"@id":"toot:featured","@type":"@id"},"alsoKnownAs":{"@id":"as:alsoKnownAs","@type":"@id"},"movedTo":{"@id":"as:movedTo","@type":"@id"},"schema":"http://schema.org#","PropertyValue":"schema:PropertyValue","value":"schema:value","Hashtag":"as:Hashtag","Emoji":"toot:Emoji","IdentityProof":"toot:IdentityProof","focalPoint":{"@container":"@list","@id":"toot:focalPoint"}}],"id":"https://mastodon.social/users/emelie","type":"Person","following":"https://mastodon.social/users/emelie/following","followers":"https://mastodon.social/users/emelie/followers","inbox":"https://mastodon.social/users/emelie/inbox","outbox":"https://mastodon.social/users/emelie/outbox","featured":"https://mastodon.social/users/emelie/collections/featured","preferredUsername":"emelie","name":"emelie 🎨","summary":"\u003cp\u003e23 / \u003ca href=\"https://mastodon.social/tags/sweden\" class=\"mention hashtag\" rel=\"tag\"\u003e#\u003cspan\u003eSweden\u003c/span\u003e\u003c/a\u003e / \u003ca href=\"https://mastodon.social/tags/artist\" class=\"mention hashtag\" rel=\"tag\"\u003e#\u003cspan\u003eArtist\u003c/span\u003e\u003c/a\u003e / \u003ca href=\"https://mastodon.social/tags/equestrian\" class=\"mention hashtag\" rel=\"tag\"\u003e#\u003cspan\u003eEquestrian\u003c/span\u003e\u003c/a\u003e / \u003ca href=\"https://mastodon.social/tags/gamedev\" class=\"mention hashtag\" rel=\"tag\"\u003e#\u003cspan\u003eGameDev\u003c/span\u003e\u003c/a\u003e\u003c/p\u003e\u003cp\u003eIf I ain\u0026apos;t spending time with my pets, I\u0026apos;m probably drawing. 🐴 🐱 🐰\u003c/p\u003e","url":"https://mastodon.social/@emelie","manuallyApprovesFollowers":false,"publicKey":{"id":"https://mastodon.social/users/emelie#main-key","owner":"https://mastodon.social/users/emelie","publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu3CWs1oAJPE3ZJ9sj6Ut\n/Mu+mTE7MOijsQc8/6c73XVVuhIEomiozJIH7l8a7S1n5SYL4UuiwcubSOi7u1bb\nGpYnp5TYhN+Cxvq/P80V4/ncNIPSQzS49it7nSLeG5pA21lGPDA44huquES1un6p\n9gSmbTwngVX9oe4MYuUeh0Z7vijjU13Llz1cRq/ZgPQPgfz+2NJf+VeXnvyDZDYx\nZPVBBlrMl3VoGbu0M5L8SjY35559KCZ3woIvqRolcoHXfgvJMdPcJgSZVYxlCw3d\nA95q9jQcn6s87CPSUs7bmYEQCrDVn5m5NER5TzwBmP4cgJl9AaDVWQtRd4jFZNTx\nlQIDAQAB\n-----END PUBLIC KEY-----\n"},"tag":[{"type":"Hashtag","href":"https://mastodon.social/explore/sweden","name":"#sweden"},{"type":"Hashtag","href":"https://mastodon.social/explore/gamedev","name":"#gamedev"},{"type":"Hashtag","href":"https://mastodon.social/explore/artist","name":"#artist"},{"type":"Hashtag","href":"https://mastodon.social/explore/equestrian","name":"#equestrian"}],"attachment":[{"type":"PropertyValue","name":"Ko-fi","value":"\u003ca href=\"https://ko-fi.com/emeliepng\" rel=\"me nofollow noopener\" target=\"_blank\"\u003e\u003cspan class=\"invisible\"\u003ehttps://\u003c/span\u003e\u003cspan class=\"\"\u003eko-fi.com/emeliepng\u003c/span\u003e\u003cspan class=\"invisible\"\u003e\u003c/span\u003e\u003c/a\u003e"},{"type":"PropertyValue","name":"Instagram","value":"\u003ca href=\"https://www.instagram.com/emelie_png/\" rel=\"me nofollow noopener\" target=\"_blank\"\u003e\u003cspan class=\"invisible\"\u003ehttps://www.\u003c/span\u003e\u003cspan class=\"\"\u003einstagram.com/emelie_png/\u003c/span\u003e\u003cspan class=\"invisible\"\u003e\u003c/span\u003e\u003c/a\u003e"},{"type":"PropertyValue","name":"Carrd","value":"\u003ca href=\"https://emelie.carrd.co/\" rel=\"me nofollow noopener\" target=\"_blank\"\u003e\u003cspan class=\"invisible\"\u003ehttps://\u003c/span\u003e\u003cspan class=\"\"\u003eemelie.carrd.co/\u003c/span\u003e\u003cspan class=\"invisible\"\u003e\u003c/span\u003e\u003c/a\u003e"},{"type":"PropertyValue","name":"Artstation","value":"\u003ca href=\"https://emiri.artstation.com\" rel=\"me nofollow noopener\" target=\"_blank\"\u003e\u003cspan class=\"invisible\"\u003ehttps://\u003c/span\u003e\u003cspan class=\"\"\u003eemiri.artstation.com\u003c/span\u003e\u003cspan class=\"invisible\"\u003e\u003c/span\u003e\u003c/a\u003e"}],"endpoints":{"sharedInbox":"https://mastodon.social/inbox"},"icon":{"type":"Image","mediaType":"image/png","url":"https://files.mastodon.social/accounts/avatars/000/015/657/original/e7163f98280da1a4.png"},"image":{"type":"Image","mediaType":"image/png","url":"https://files.mastodon.social/accounts/headers/000/015/657/original/847f331f3dd9e38b.png"}} \ No newline at end of file From f2efe24cf8b668de160bb893d2b40a7c6151ba78 Mon Sep 17 00:00:00 2001 From: William Pitcock Date: Sat, 1 Jun 2019 03:23:21 +0000 Subject: [PATCH 37/52] visibility: if litepub:directMessage is asserted, always report the object as "direct" visibility --- lib/pleroma/web/activity_pub/visibility.ex | 3 +++ test/web/activity_pub/visibilty_test.exs | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/lib/pleroma/web/activity_pub/visibility.ex b/lib/pleroma/web/activity_pub/visibility.ex index 93b50ee47..8965e3253 100644 --- a/lib/pleroma/web/activity_pub/visibility.ex +++ b/lib/pleroma/web/activity_pub/visibility.ex @@ -66,6 +66,9 @@ def get_visibility(object) do Enum.any?(to, &String.contains?(&1, "/followers")) -> "private" + object.data["directMessage"] == true -> + "direct" + length(cc) > 0 -> "private" diff --git a/test/web/activity_pub/visibilty_test.exs b/test/web/activity_pub/visibilty_test.exs index e2584f635..466d980dc 100644 --- a/test/web/activity_pub/visibilty_test.exs +++ b/test/web/activity_pub/visibilty_test.exs @@ -117,4 +117,8 @@ test "get_visibility", %{ assert Visibility.get_visibility(direct) == "direct" assert Visibility.get_visibility(unlisted) == "unlisted" end + + test "get_visibility with directMessage flag" do + assert Visibility.get_visibility(%{data: %{"directMessage" => true}}) == "direct" + end end From 2bbc2a801f4f164b9b1beaeedc29a80be2cfbaec Mon Sep 17 00:00:00 2001 From: Sergey Suprunenko Date: Sat, 1 Jun 2019 03:26:45 +0000 Subject: [PATCH 38/52] Remove all follower collections but actor's from "cc" --- .../web/activity_pub/transmogrifier.ex | 14 ++-- .../activity_pub_controller_test.exs | 71 +++++++++++++++---- test/web/activity_pub/transmogrifier_test.exs | 25 +++++-- 3 files changed, 86 insertions(+), 24 deletions(-) diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index d8fa2728d..f18ffdee6 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -65,7 +65,11 @@ def fix_addressing_list(map, field) do end end - def fix_explicit_addressing(%{"to" => to, "cc" => cc} = object, explicit_mentions) do + def fix_explicit_addressing( + %{"to" => to, "cc" => cc} = object, + explicit_mentions, + follower_collection + ) do explicit_to = to |> Enum.filter(fn x -> x in explicit_mentions end) @@ -76,6 +80,7 @@ def fix_explicit_addressing(%{"to" => to, "cc" => cc} = object, explicit_mention final_cc = (cc ++ explicit_cc) + |> Enum.reject(fn x -> String.ends_with?(x, "/followers") and x != follower_collection end) |> Enum.uniq() object @@ -83,7 +88,7 @@ def fix_explicit_addressing(%{"to" => to, "cc" => cc} = object, explicit_mention |> Map.put("cc", final_cc) end - def fix_explicit_addressing(object, _explicit_mentions), do: object + def fix_explicit_addressing(object, _explicit_mentions, _followers_collection), do: object # if directMessage flag is set to true, leave the addressing alone def fix_explicit_addressing(%{"directMessage" => true} = object), do: object @@ -98,8 +103,7 @@ def fix_explicit_addressing(object) do explicit_mentions = explicit_mentions ++ ["https://www.w3.org/ns/activitystreams#Public", follower_collection] - object - |> fix_explicit_addressing(explicit_mentions) + fix_explicit_addressing(object, explicit_mentions, follower_collection) end # if as:Public is addressed, then make sure the followers collection is also addressed @@ -136,7 +140,7 @@ def fix_addressing(object) do |> fix_addressing_list("cc") |> fix_addressing_list("bto") |> fix_addressing_list("bcc") - |> fix_explicit_addressing + |> fix_explicit_addressing() |> fix_implicit_addressing(followers_collection) end diff --git a/test/web/activity_pub/activity_pub_controller_test.exs b/test/web/activity_pub/activity_pub_controller_test.exs index 30adfda36..8b3233729 100644 --- a/test/web/activity_pub/activity_pub_controller_test.exs +++ b/test/web/activity_pub/activity_pub_controller_test.exs @@ -11,6 +11,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do alias Pleroma.User alias Pleroma.Web.ActivityPub.ObjectView alias Pleroma.Web.ActivityPub.UserView + alias Pleroma.Web.ActivityPub.Utils setup_all do Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) @@ -234,13 +235,17 @@ test "it clears `unreachable` federation status of the sender", %{conn: conn} do end describe "/users/:nickname/inbox" do - test "it inserts an incoming activity into the database", %{conn: conn} do - user = insert(:user) - + setup do data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!() - |> Map.put("bcc", [user.ap_id]) + + [data: data] + end + + test "it inserts an incoming activity into the database", %{conn: conn, data: data} do + user = insert(:user) + data = Map.put(data, "bcc", [user.ap_id]) conn = conn @@ -253,16 +258,15 @@ test "it inserts an incoming activity into the database", %{conn: conn} do assert Activity.get_by_ap_id(data["id"]) end - test "it accepts messages from actors that are followed by the user", %{conn: conn} do + test "it accepts messages from actors that are followed by the user", %{ + conn: conn, + data: data + } do recipient = insert(:user) actor = insert(:user, %{ap_id: "http://mastodon.example.org/users/actor"}) {:ok, recipient} = User.follow(recipient, actor) - data = - File.read!("test/fixtures/mastodon-post-activity.json") - |> Poison.decode!() - object = data["object"] |> Map.put("attributedTo", actor.ap_id) @@ -309,13 +313,9 @@ test "it returns a note activity in a collection", %{conn: conn} do assert response(conn, 200) =~ note_activity.data["object"]["content"] end - test "it clears `unreachable` federation status of the sender", %{conn: conn} do + test "it clears `unreachable` federation status of the sender", %{conn: conn, data: data} do user = insert(:user) - - data = - File.read!("test/fixtures/mastodon-post-activity.json") - |> Poison.decode!() - |> Map.put("bcc", [user.ap_id]) + data = Map.put(data, "bcc", [user.ap_id]) sender_host = URI.parse(data["actor"]).host Instances.set_consistently_unreachable(sender_host) @@ -330,6 +330,47 @@ test "it clears `unreachable` federation status of the sender", %{conn: conn} do assert "ok" == json_response(conn, 200) assert Instances.reachable?(sender_host) end + + test "it removes all follower collections but actor's", %{conn: conn} do + [actor, recipient] = insert_pair(:user) + + data = + File.read!("test/fixtures/activitypub-client-post-activity.json") + |> Poison.decode!() + + object = Map.put(data["object"], "attributedTo", actor.ap_id) + + data = + data + |> Map.put("id", Utils.generate_object_id()) + |> Map.put("actor", actor.ap_id) + |> Map.put("object", object) + |> Map.put("cc", [ + recipient.follower_address, + actor.follower_address + ]) + |> Map.put("to", [ + recipient.ap_id, + recipient.follower_address, + "https://www.w3.org/ns/activitystreams#Public" + ]) + + conn + |> assign(:valid_signature, true) + |> put_req_header("content-type", "application/activity+json") + |> post("/users/#{recipient.nickname}/inbox", data) + |> json_response(200) + + activity = Activity.get_by_ap_id(data["id"]) + + assert activity.id + assert actor.follower_address in activity.recipients + assert actor.follower_address in activity.data["cc"] + + refute recipient.follower_address in activity.recipients + refute recipient.follower_address in activity.data["cc"] + refute recipient.follower_address in activity.data["to"] + end end describe "/users/:nickname/outbox" do diff --git a/test/web/activity_pub/transmogrifier_test.exs b/test/web/activity_pub/transmogrifier_test.exs index ee71de8d0..bcc460f1c 100644 --- a/test/web/activity_pub/transmogrifier_test.exs +++ b/test/web/activity_pub/transmogrifier_test.exs @@ -1211,9 +1211,12 @@ test "successfully reserializes a message with AS2 objects in IR" do end describe "fix_explicit_addressing" do - test "moves non-explicitly mentioned actors to cc" do + setup do user = insert(:user) + [user: user] + end + test "moves non-explicitly mentioned actors to cc", %{user: user} do explicitly_mentioned_actors = [ "https://pleroma.gold/users/user1", "https://pleroma.gold/user2" @@ -1235,9 +1238,7 @@ test "moves non-explicitly mentioned actors to cc" do assert "https://social.beepboop.ga/users/dirb" in fixed_object["cc"] end - test "does not move actor's follower collection to cc" do - user = insert(:user) - + test "does not move actor's follower collection to cc", %{user: user} do object = %{ "actor" => user.ap_id, "to" => [user.follower_address], @@ -1248,5 +1249,21 @@ test "does not move actor's follower collection to cc" do assert user.follower_address in fixed_object["to"] refute user.follower_address in fixed_object["cc"] end + + test "removes recipient's follower collection from cc", %{user: user} do + recipient = insert(:user) + + object = %{ + "actor" => user.ap_id, + "to" => [recipient.ap_id, "https://www.w3.org/ns/activitystreams#Public"], + "cc" => [user.follower_address, recipient.follower_address] + } + + fixed_object = Transmogrifier.fix_explicit_addressing(object) + + assert user.follower_address in fixed_object["cc"] + refute recipient.follower_address in fixed_object["cc"] + refute recipient.follower_address in fixed_object["to"] + end end end From 300d94c62829d0ec961f3ed6c0242dea102ab0ad Mon Sep 17 00:00:00 2001 From: rinpatch Date: Sat, 1 Jun 2019 16:07:01 +0300 Subject: [PATCH 39/52] Add poll votes Also in this commit by accident: - Fix query ordering causing exclude_poll_votes to not work - Do not create notifications for Answer objects --- lib/pleroma/notification.ex | 11 ++- lib/pleroma/web/activity_pub/activity_pub.ex | 2 +- lib/pleroma/web/activity_pub/utils.ex | 17 +++++ lib/pleroma/web/common_api/common_api.ex | 46 +++++++++++ lib/pleroma/web/common_api/utils.ex | 11 +++ .../mastodon_api/mastodon_api_controller.ex | 27 +++++++ lib/pleroma/web/router.ex | 2 + .../mastodon_api_controller_test.exs | 76 +++++++++++++++++++ 8 files changed, 188 insertions(+), 4 deletions(-) diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index 844264307..80e2800ae 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -127,10 +127,15 @@ def dismiss(%{id: user_id} = _user, id) do def create_notifications(%Activity{data: %{"to" => _, "type" => type}} = activity) when type in ["Create", "Like", "Announce", "Follow"] do - users = get_notified_from_activity(activity) + object = Object.normalize(activity) - notifications = Enum.map(users, fn user -> create_notification(activity, user) end) - {:ok, notifications} + unless object && object.data["type"] == "Answer" do + users = get_notified_from_activity(activity) + notifications = Enum.map(users, fn user -> create_notification(activity, user) end) + {:ok, notifications} + else + {:ok, []} + end end def create_notifications(_), do: {:ok, []} diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index b06200cb5..5a11dadcf 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -878,7 +878,6 @@ def fetch_activities_query(recipients, opts \\ %{}) do |> maybe_preload_objects(opts) |> maybe_preload_bookmarks(opts) |> maybe_order(opts) - |> exclude_poll_votes(opts) |> restrict_recipients(recipients, opts["user"]) |> restrict_tag(opts) |> restrict_tag_reject(opts) @@ -899,6 +898,7 @@ def fetch_activities_query(recipients, opts \\ %{}) do |> restrict_pinned(opts) |> restrict_muted_reblogs(opts) |> Activity.restrict_deactivated_users() + |> exclude_poll_votes(opts) end def fetch_activities(recipients, opts \\ %{}) do diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index 9646bbee9..b292d7d8d 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -789,4 +789,21 @@ defp get_updated_targets( [to, cc, recipients] end end + + def get_existing_votes(actor, %{data: %{"id" => id}}) do + query = + from( + [activity, object: object] in Activity.with_preloaded_object(Activity), + where: fragment("(?)->>'actor' = ?", activity.data, ^actor), + where: + fragment( + "(?)->'inReplyTo' = ?", + object.data, + ^to_string(id) + ), + where: fragment("(?)->>'type' = 'Answer'", object.data) + ) + + Repo.all(query) + end end diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index 374967a1b..f54f8a7b9 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -119,6 +119,52 @@ def unfavorite(id_or_ap_id, user) do end end + def vote(user, object, choices) do + with "Question" <- object.data["type"], + {:author, false} <- {:author, object.data["actor"] == user.ap_id}, + {:existing_votes, []} <- {:existing_votes, Utils.get_existing_votes(user.ap_id, object)}, + {options, max_count} <- get_options_and_max_count(object), + option_count <- Enum.count(options), + {:choice_check, {choices, true}} <- + {:choice_check, normalize_and_validate_choice_indices(choices, option_count)}, + {:count_check, true} <- {:count_check, Enum.count(choices) <= max_count} do + answer_activities = + Enum.map(choices, fn index -> + answer_data = make_answer_data(user, object, Enum.at(options, index)["name"]) + + ActivityPub.create(%{ + to: answer_data["to"], + actor: user, + context: object.data["context"], + object: answer_data, + additional: %{"cc" => answer_data["cc"]} + }) + end) + + {:ok, answer_activities, object} + else + {:author, _} -> {:error, "Already voted"} + {:existing_votes, _} -> {:error, "Already voted"} + {:choice_check, {_, false}} -> {:error, "Invalid indices"} + {:count_check, false} -> {:error, "Too many choices"} + end + end + + defp get_options_and_max_count(object) do + if Map.has_key?(object.data, "anyOf") do + {object.data["anyOf"], Enum.count(object.data["anyOf"])} + else + {object.data["oneOf"], 1} + end + end + + defp normalize_and_validate_choice_indices(choices, count) do + Enum.map_reduce(choices, true, fn index, valid -> + index = if is_binary(index), do: String.to_integer(index), else: index + {index, if(valid, do: index < count, else: valid)} + end) + end + def get_visibility(%{"visibility" => visibility}, in_reply_to) when visibility in ~w{public unlisted private direct}, do: {visibility, get_replied_to_visibility(in_reply_to)} diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index 1a239de97..f35ed36ab 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -491,4 +491,15 @@ def conversation_id_to_context(id) do {:error, "No such conversation"} end end + + def make_answer_data(%User{ap_id: ap_id}, object, name) do + %{ + "type" => "Answer", + "actor" => ap_id, + "cc" => [object.data["actor"]], + "to" => [], + "name" => name, + "inReplyTo" => object.data["id"] + } + end end diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index ecb7df459..13bb609e5 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -430,6 +430,33 @@ def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do end end + def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do + with %Object{} = object <- Object.get_by_id(id), + true <- object.data["type"] == "Question", + %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]), + true <- Visibility.visible_for_user?(activity, user), + {:ok, _activities, object} <- CommonAPI.vote(user, object, choices) do + conn + |> put_view(StatusView) + |> try_render("poll.json", %{object: object, for: user}) + else + nil -> + conn + |> put_status(404) + |> json(%{error: "Record not found"}) + + false -> + conn + |> put_status(404) + |> json(%{error: "Record not found"}) + + {:error, message} -> + conn + |> put_status(422) + |> json(%{error: message}) + end + end + def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do conn diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index e0611e3fc..d17f58f52 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -335,6 +335,8 @@ defmodule Pleroma.Web.Router do put("/scheduled_statuses/:id", MastodonAPIController, :update_scheduled_status) delete("/scheduled_statuses/:id", MastodonAPIController, :delete_scheduled_status) + post("/polls/:id/votes", MastodonAPIController, :poll_vote) + post("/media", MastodonAPIController, :upload) put("/media/:id", MastodonAPIController, :update_media) diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs index 0d56b6ff2..b160a4db0 100644 --- a/test/web/mastodon_api/mastodon_api_controller_test.exs +++ b/test/web/mastodon_api/mastodon_api_controller_test.exs @@ -3497,4 +3497,80 @@ test "does not expose polls for private statuses", %{conn: conn} do assert json_response(conn, 404) end end + + describe "POST /api/v1/polls/:id/votes" do + test "votes are added to the poll", %{conn: conn} do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + "status" => "A very delicious sandwich", + "poll" => %{ + "options" => ["Lettuce", "Grilled Bacon", "Tomato"], + "expires_in" => 20, + "multiple" => true + } + }) + + object = Object.normalize(activity) + + conn = + conn + |> assign(:user, other_user) + |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [0, 1, 2]}) + + assert json_response(conn, 200) + object = Object.get_by_id(object.id) + + assert Enum.all?(object.data["anyOf"], fn %{"replies" => %{"totalItems" => totalItems}} -> + totalItems == 1 + end) + end + + test "author can't vote", %{conn: conn} do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + "status" => "Am I cute?", + "poll" => %{"options" => ["Yes", "No"], "expires_in" => 20} + }) + + object = Object.normalize(activity) + + assert conn + |> assign(:user, user) + |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [1]}) + |> json_response(422) == %{"error" => "Already voted"} + + object = Object.get_by_id(object.id) + + refute Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 1 + end + + test "does not allow multiple choices on a single-choice question", %{conn: conn} do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + "status" => "The glass is", + "poll" => %{"options" => ["half empty", "half full"], "expires_in" => 20} + }) + + object = Object.normalize(activity) + + assert conn + |> assign(:user, other_user) + |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [0, 1]}) + |> json_response(422) == %{"error" => "Too many choices"} + + object = Object.get_by_id(object.id) + + refute Enum.any?(object.data["oneOf"], fn %{"replies" => %{"totalItems" => totalItems}} -> + totalItems == 1 + end) + end + end end From 444406167b050524efb016cfee78636f7f6828ca Mon Sep 17 00:00:00 2001 From: rinpatch Date: Sat, 1 Jun 2019 21:41:23 +0300 Subject: [PATCH 40/52] Mastodon API: actually check for poll votes --- lib/pleroma/web/mastodon_api/views/status_view.ex | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 5bf4a6ba2..7eea0122b 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -331,7 +331,7 @@ def render("attachment.json", %{attachment: attachment}) do end # TODO: Add tests for this view - def render("poll.json", %{object: object} = _opts) do + def render("poll.json", %{object: object} = opts) do {multiple, options} = case object.data do %{"anyOf" => options} when is_list(options) -> {true, options} @@ -352,6 +352,16 @@ def render("poll.json", %{object: object} = _opts) do _ -> false end + voted = + if opts[:for] do + existing_votes = + Pleroma.Web.ActivityPub.Utils.get_existing_votes(opts[:for].ap_id, object) + + existing_votes != [] or opts[:for].ap_id == object.data["actor"] + else + false + end + {options, votes_count} = Enum.map_reduce(options, 0, fn %{"name" => name} = option, count -> current_count = option["replies"]["totalItems"] || 0 @@ -371,8 +381,7 @@ def render("poll.json", %{object: object} = _opts) do multiple: multiple, votes_count: votes_count, options: options, - # TODO: Actually check for a vote - voted: false, + voted: voted, emojis: build_emojis(object.data["emoji"]) } else From 6bc9e5c020d97edcaaa386bd2ec8e8ec57cb19af Mon Sep 17 00:00:00 2001 From: rinpatch Date: Sat, 1 Jun 2019 21:41:49 +0300 Subject: [PATCH 41/52] Mastodon API: Refresh the object before rendering it after voting --- lib/pleroma/web/mastodon_api/mastodon_api_controller.ex | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index bab6d693d..8da31161f 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -436,6 +436,8 @@ def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choic %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]), true <- Visibility.visible_for_user?(activity, user), {:ok, _activities, object} <- CommonAPI.vote(user, object, choices) do + object = Object.get_cached_by_ap_id(object.data["id"]) + conn |> put_view(StatusView) |> try_render("poll.json", %{object: object, for: user}) From cfa588e3574f3083798de6f4e0eca63f00aaf578 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Sat, 1 Jun 2019 21:42:29 +0300 Subject: [PATCH 42/52] Fix Credo issues --- test/web/mastodon_api/mastodon_api_controller_test.exs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs index 24fd319d3..9c1db37db 100644 --- a/test/web/mastodon_api/mastodon_api_controller_test.exs +++ b/test/web/mastodon_api/mastodon_api_controller_test.exs @@ -3630,8 +3630,8 @@ test "votes are added to the poll", %{conn: conn} do assert json_response(conn, 200) object = Object.get_by_id(object.id) - assert Enum.all?(object.data["anyOf"], fn %{"replies" => %{"totalItems" => totalItems}} -> - totalItems == 1 + assert Enum.all?(object.data["anyOf"], fn %{"replies" => %{"totalItems" => total_items}} -> + total_items == 1 end) end @@ -3675,8 +3675,8 @@ test "does not allow multiple choices on a single-choice question", %{conn: conn object = Object.get_by_id(object.id) - refute Enum.any?(object.data["oneOf"], fn %{"replies" => %{"totalItems" => totalItems}} -> - totalItems == 1 + refute Enum.any?(object.data["oneOf"], fn %{"replies" => %{"totalItems" => total_items}} -> + total_items == 1 end) end end From 67bcc3ccc433bd5c7b59f5bf1a45db93da93d093 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Sat, 1 Jun 2019 21:49:18 +0300 Subject: [PATCH 43/52] Remove index on object name used for excluding poll votes (replaced by just rewriting Note to Answer) --- .../migrations/20190522084924_add_object_index_on_name.exs | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 priv/repo/migrations/20190522084924_add_object_index_on_name.exs diff --git a/priv/repo/migrations/20190522084924_add_object_index_on_name.exs b/priv/repo/migrations/20190522084924_add_object_index_on_name.exs deleted file mode 100644 index 886e8499e..000000000 --- a/priv/repo/migrations/20190522084924_add_object_index_on_name.exs +++ /dev/null @@ -1,7 +0,0 @@ -defmodule Pleroma.Repo.Migrations.AddObjectIndexOnName do - use Ecto.Migration - - def change do - create(index(:objects, ["(data->'name')"], name: :objects_name_index, using: :gin)) - end -end From 52e09807d4b118b17c2f504f6224218a047440d8 Mon Sep 17 00:00:00 2001 From: William Pitcock Date: Sun, 2 Jun 2019 09:09:58 +0000 Subject: [PATCH 44/52] reverse proxy: clean up some @hackney leftovers --- lib/pleroma/reverse_proxy.ex | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/pleroma/reverse_proxy.ex b/lib/pleroma/reverse_proxy.ex index 983e156f5..285d57309 100644 --- a/lib/pleroma/reverse_proxy.ex +++ b/lib/pleroma/reverse_proxy.ex @@ -61,8 +61,6 @@ defmodule Pleroma.ReverseProxy do * `http`: options for [hackney](https://github.com/benoitc/hackney). """ - @hackney Pleroma.Config.get(:hackney, :hackney) - @default_hackney_options [] @inline_content_types [ @@ -148,7 +146,7 @@ defp request(method, url, headers, hackney_opts) do Logger.debug("#{__MODULE__} #{method} #{url} #{inspect(headers)}") method = method |> String.downcase() |> String.to_existing_atom() - case @hackney.request(method, url, headers, "", hackney_opts) do + case :hackney.request(method, url, headers, "", hackney_opts) do {:ok, code, headers, client} when code in @valid_resp_codes -> {:ok, code, downcase_headers(headers), client} @@ -198,7 +196,7 @@ defp chunk_reply(conn, client, opts, sent_so_far, duration) do duration, Keyword.get(opts, :max_read_duration, @max_read_duration) ), - {:ok, data} <- @hackney.stream_body(client), + {:ok, data} <- :hackney.stream_body(client), {:ok, duration} <- increase_read_duration(duration), sent_so_far = sent_so_far + byte_size(data), :ok <- body_size_constraint(sent_so_far, Keyword.get(opts, :max_body_size)), From e3c460353dacb796a867a0a3afa3632746d57aa2 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Sun, 2 Jun 2019 23:24:48 +0300 Subject: [PATCH 45/52] Refresh the object in CommonAPI.vote instead of MastoAPI controller --- lib/pleroma/web/common_api/common_api.ex | 1 + lib/pleroma/web/mastodon_api/mastodon_api_controller.ex | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index f54f8a7b9..a12ee011b 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -141,6 +141,7 @@ def vote(user, object, choices) do }) end) + object = Object.get_cached_by_ap_id(object.data["id"]) {:ok, answer_activities, object} else {:author, _} -> {:error, "Already voted"} diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index 8da31161f..bab6d693d 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -436,8 +436,6 @@ def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choic %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]), true <- Visibility.visible_for_user?(activity, user), {:ok, _activities, object} <- CommonAPI.vote(user, object, choices) do - object = Object.get_cached_by_ap_id(object.data["id"]) - conn |> put_view(StatusView) |> try_render("poll.json", %{object: object, for: user}) From c47da0e65da32848b1f7e8cecce33a46d6f0115f Mon Sep 17 00:00:00 2001 From: rinpatch Date: Sun, 2 Jun 2019 23:25:33 +0300 Subject: [PATCH 46/52] Add tests for poll view --- test/web/mastodon_api/status_view_test.exs | 102 +++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/test/web/mastodon_api/status_view_test.exs b/test/web/mastodon_api/status_view_test.exs index 9f2ebda4e..ec75150ab 100644 --- a/test/web/mastodon_api/status_view_test.exs +++ b/test/web/mastodon_api/status_view_test.exs @@ -342,4 +342,106 @@ test "a rich media card with all relevant data renders correctly" do StatusView.render("card.json", %{page_url: page_url, rich_media: card}) end end + + describe "poll view" do + test "renders a poll" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + "status" => "Is Tenshi eating a corndog cute?", + "poll" => %{ + "options" => ["absolutely!", "sure", "yes", "why are you even asking?"], + "expires_in" => 20 + } + }) + + object = Object.normalize(activity) + + expected = %{ + emojis: [], + expired: false, + id: object.id, + multiple: false, + options: [ + %{title: "absolutely!", votes_count: 0}, + %{title: "sure", votes_count: 0}, + %{title: "yes", votes_count: 0}, + %{title: "why are you even asking?", votes_count: 0} + ], + voted: false, + votes_count: 0 + } + + result = StatusView.render("poll.json", %{object: object}) + expires_at = result.expires_at + result = Map.delete(result, :expires_at) + + assert result == expected + + expires_at = NaiveDateTime.from_iso8601!(expires_at) + assert NaiveDateTime.diff(expires_at, NaiveDateTime.utc_now()) in 15..20 + end + + test "detects if it is multiple choice" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + "status" => "Which Mastodon developer is your favourite?", + "poll" => %{ + "options" => ["Gargron", "Eugen"], + "expires_in" => 20, + "multiple" => true + } + }) + + object = Object.normalize(activity) + + assert %{multiple: true} = StatusView.render("poll.json", %{object: object}) + end + + test "detects emoji" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + "status" => "What's with the smug face?", + "poll" => %{ + "options" => [":blank: sip", ":blank::blank: sip", ":blank::blank::blank: sip"], + "expires_in" => 20 + } + }) + + object = Object.normalize(activity) + + assert %{emojis: [%{shortcode: "blank"}]} = + StatusView.render("poll.json", %{object: object}) + end + + test "detects vote status" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + "status" => "Which input devices do you use?", + "poll" => %{ + "options" => ["mouse", "trackball", "trackpoint"], + "multiple" => true, + "expires_in" => 20 + } + }) + + object = Object.normalize(activity) + + {:ok, _, object} = CommonAPI.vote(other_user, object, [1, 2]) + + result = StatusView.render("poll.json", %{object: object, for: other_user}) + + assert result[:voted] == true + assert Enum.at(result[:options], 1)[:votes_count] == 1 + assert Enum.at(result[:options], 2)[:votes_count] == 1 + end + end end From 2fe3a20638789a8fb6e1a8e63cd5eb2247a9308a Mon Sep 17 00:00:00 2001 From: rinpatch Date: Sun, 2 Jun 2019 23:30:36 +0300 Subject: [PATCH 47/52] Make error message about author's inability to vote more sensible --- lib/pleroma/web/common_api/common_api.ex | 2 +- test/web/mastodon_api/mastodon_api_controller_test.exs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index a12ee011b..5212d5ce5 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -144,7 +144,7 @@ def vote(user, object, choices) do object = Object.get_cached_by_ap_id(object.data["id"]) {:ok, answer_activities, object} else - {:author, _} -> {:error, "Already voted"} + {:author, _} -> {:error, "Poll's author can't vote"} {:existing_votes, _} -> {:error, "Already voted"} {:choice_check, {_, false}} -> {:error, "Invalid indices"} {:count_check, false} -> {:error, "Too many choices"} diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs index 9c1db37db..c158c4df2 100644 --- a/test/web/mastodon_api/mastodon_api_controller_test.exs +++ b/test/web/mastodon_api/mastodon_api_controller_test.exs @@ -3649,7 +3649,7 @@ test "author can't vote", %{conn: conn} do assert conn |> assign(:user, user) |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [1]}) - |> json_response(422) == %{"error" => "Already voted"} + |> json_response(422) == %{"error" => "Poll's author can't vote"} object = Object.get_by_id(object.id) From 1fd8e19d767e03475555e879c8e5e0fcc8d5c6bf Mon Sep 17 00:00:00 2001 From: rinpatch Date: Sun, 2 Jun 2019 23:46:17 +0300 Subject: [PATCH 48/52] Remove a TODO comment as the tests for poll view were written --- lib/pleroma/web/mastodon_api/views/status_view.ex | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 7eea0122b..6836d331a 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -330,7 +330,6 @@ def render("attachment.json", %{attachment: attachment}) do } end - # TODO: Add tests for this view def render("poll.json", %{object: object} = opts) do {multiple, options} = case object.data do From baefb97dc4f4435027f6e23e77433e4bfaf0dcd1 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Mon, 3 Jun 2019 10:55:16 +0300 Subject: [PATCH 49/52] Add a changelog entry for polls --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff1fff876..2144bbe28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Mix Tasks: `mix pleroma.database remove_embedded_objects` - Mix Tasks: `mix pleroma.database update_users_following_followers_counts` - Mix Tasks: `mix pleroma.user toggle_confirmed` +- Federation: Support for `Question` and `Answer` objects - Federation: Support for reports +- Configuration: `poll_limits` option - Configuration: `safe_dm_mentions` option - Configuration: `link_name` option - Configuration: `fetch_initial_posts` option @@ -37,6 +39,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Mastodon API: `/api/v1/pleroma/accounts/:id/favourites` (API extension) - Mastodon API: [Reports](https://docs.joinmastodon.org/api/rest/reports/) - Mastodon API: `POST /api/v1/accounts` (account creation API) +- Mastodon API: [Polls](https://docs.joinmastodon.org/api/rest/polls/) - ActivityPub C2S: OAuth endpoints - Metadata: RelMe provider - OAuth: added support for refresh tokens From 5bd41fef8b5aeff53ed6b096e04507d51c93a83a Mon Sep 17 00:00:00 2001 From: rinpatch Date: Mon, 3 Jun 2019 10:58:37 +0300 Subject: [PATCH 50/52] Change query order in fetch_activities_for_context_query to make poll vote exclusion work --- lib/pleroma/web/activity_pub/activity_pub.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index d494d5cd1..45feae25a 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -489,7 +489,6 @@ defp fetch_activities_for_context_query(context, opts) do from(activity in Activity) |> maybe_preload_objects(opts) - |> exclude_poll_votes(opts) |> restrict_blocked(opts) |> restrict_recipients(recipients, opts["user"]) |> where( @@ -502,6 +501,7 @@ defp fetch_activities_for_context_query(context, opts) do ^context ) ) + |> exclude_poll_votes(opts) |> order_by([activity], desc: activity.id) end From c3bcc4f6a2d23574de2feafe8b649745a9b96179 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Mon, 3 Jun 2019 11:24:03 +0300 Subject: [PATCH 51/52] CI: Replace mix test and mix coveralls with just mix coveralls `mix coveralls` runs the tests by itself, exits with failure if one of them fails and accepts the same args, so there is no reason to run two of them at the same time. --- .gitlab-ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 53f05ae92..939175218 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -52,8 +52,7 @@ unit-testing: - mix deps.get - mix ecto.create - mix ecto.migrate - - mix test --trace --preload-modules - - mix coveralls + - mix coveralls --trace --preload-modules unit-testing-rum: stage: test From 9ded9443a3551360991dfa28ced07502037c4559 Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 3 Jun 2019 11:47:10 +0200 Subject: [PATCH 52/52] CI: Actually push to correct repo. --- .gitlab-ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 939175218..58c9de167 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -121,8 +121,7 @@ review_app: - (ssh -t dokku@pleroma.online -- postgres:create $(echo $CI_ENVIRONMENT_SLUG | sed -e 's/-/_/g')_db) || true - (ssh -t dokku@pleroma.online -- postgres:link $(echo $CI_ENVIRONMENT_SLUG | sed -e 's/-/_/g')_db "$CI_ENVIRONMENT_SLUG") || true - (ssh -t dokku@pleroma.online -- certs:add "$CI_ENVIRONMENT_SLUG" /home/dokku/server.crt /home/dokku/server.key) || true - - (git remote add dokku dokku@pleroma.online:$CI_ENVIRONMENT_SLUG) || true - - git push -f dokku $CI_COMMIT_SHA:refs/heads/master + - git push -f dokku@pleroma.online:$CI_ENVIRONMENT_SLUG $CI_COMMIT_SHA:refs/heads/master stop_review_app: image: alpine:3.9