From b635937091032edd5f6222ae425b22e2db1f9793 Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Mon, 20 Nov 2023 09:35:21 +0100 Subject: [PATCH] fix: various fixes Signed-off-by: Thomas Citharel --- .../activity_pub/types/conversation.ex | 6 + lib/graphql/resolvers/conversation.ex | 14 +- lib/graphql/resolvers/participant.ex | 20 +- lib/mobilizon/discussions/comment.ex | 2 +- lib/service/activity/conversation.ex | 9 +- lib/service/formatter/html.ex | 14 ++ lib/service/formatter/text.ex | 37 ++++ .../workers/legacy_notifier_builder.ex | 7 + lib/web/email/activity.ex | 33 +++ .../email/email_anonymous_activity.html.heex | 205 +++++++++++++----- .../email/email_anonymous_activity.text.eex | 25 ++- lib/web/views/email_view.ex | 16 ++ src/assets/oruga-tailwindcss.css | 11 +- src/components/Account/ActorAutoComplete.vue | 15 +- src/components/Account/ActorCard.vue | 16 +- src/components/Account/ActorInline.vue | 4 - .../Conversations/ConversationListItem.vue | 2 +- .../Conversations/NewConversation.vue | 60 ++++- .../Participation/NewPrivateMessage.vue | 50 ++++- .../Participation/UnloggedParticipation.vue | 6 +- src/graphql/conversations.ts | 2 - src/i18n/en_US.json | 5 +- src/i18n/fr_FR.json | 5 +- src/oruga-config.ts | 3 +- src/utils/route.ts | 10 + src/views/Admin/InstancesView.vue | 2 +- .../Conversations/ConversationListView.vue | 24 +- src/views/Conversations/ConversationView.vue | 59 ++++- src/views/Event/EditView.vue | 12 +- src/views/Event/ParticipantsView.vue | 2 + src/views/Group/GroupView.vue | 15 ++ src/views/HomeView.vue | 6 +- src/views/SearchView.vue | 11 +- 33 files changed, 579 insertions(+), 129 deletions(-) create mode 100644 lib/service/formatter/text.ex create mode 100644 src/utils/route.ts diff --git a/lib/federation/activity_pub/types/conversation.ex b/lib/federation/activity_pub/types/conversation.ex index b72cef02..6bb8c791 100644 --- a/lib/federation/activity_pub/types/conversation.ex +++ b/lib/federation/activity_pub/types/conversation.ex @@ -147,6 +147,12 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Conversations do (args |> Map.get(:mentions, []) |> prepare_mentions()) ++ ConverterUtils.fetch_mentions(mentions) + # Can't create a conversation with just ourselves + mentions = + Enum.filter(mentions, fn %{actor_id: actor_id} -> + to_string(actor_id) != to_string(args.actor_id) + end) + if Enum.empty?(mentions) do {:error, :empty_participants} else diff --git a/lib/graphql/resolvers/conversation.ex b/lib/graphql/resolvers/conversation.ex index 00f95de5..33c76c33 100644 --- a/lib/graphql/resolvers/conversation.ex +++ b/lib/graphql/resolvers/conversation.ex @@ -11,8 +11,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Conversation do alias Mobilizon.Storage.Page alias Mobilizon.Users.User alias Mobilizon.Web.Endpoint - # alias Mobilizon.Users.User import Mobilizon.Web.Gettext, only: [dgettext: 2] + require Logger @spec find_conversations_for_event(Event.t(), map(), Absinthe.Resolution.t()) :: {:ok, Page.t(ConversationView.t())} | {:error, :unauthenticated} @@ -157,9 +157,17 @@ defmodule Mobilizon.GraphQL.Resolvers.Conversation do {:ok, conversation_to_view(conversation, conversation_participant_actor)} {:error, :empty_participants} -> - {:error, dgettext("errors", "Conversation needs to mention at least one participant")} + {:error, + dgettext( + "errors", + "Conversation needs to mention at least one participant that's not yourself" + )} end else + Logger.debug( + "Actor #{current_actor.id} is not authorized to reply to conversation #{inspect(Map.get(args, :conversation_id))}" + ) + {:error, :unauthorized} end end @@ -259,7 +267,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Conversation do %Conversation{participants: participants} -> participant_ids = Enum.map(participants, fn participant -> to_string(participant.id) end) - current_actor_id in participant_ids or + to_string(current_actor_id) in participant_ids or Enum.any?(participant_ids, fn participant_id -> Actors.is_member?(current_actor_id, participant_id) and attributed_to_id == participant_id diff --git a/lib/graphql/resolvers/participant.ex b/lib/graphql/resolvers/participant.ex index 520ecb14..23e73987 100644 --- a/lib/graphql/resolvers/participant.ex +++ b/lib/graphql/resolvers/participant.ex @@ -2,8 +2,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Participant do @moduledoc """ Handles the participation-related GraphQL calls. """ - # alias Mobilizon.Conversations.ConversationParticipant - alias Mobilizon.{Actors, Config, Crypto, Events} + alias Mobilizon.{Actors, Config, Conversations, Crypto, Events} alias Mobilizon.Actors.Actor alias Mobilizon.Conversations.{Conversation, ConversationView} alias Mobilizon.Events.{Event, Participant} @@ -386,6 +385,13 @@ defmodule Mobilizon.GraphQL.Resolvers.Participant do {:member, false} -> {:error, :unauthorized} + {:error, :empty_participants} -> + {:error, + dgettext( + "errors", + "There are no participants matching the audience you've selected." + )} + {:error, err} -> {:error, err} end @@ -394,11 +400,19 @@ defmodule Mobilizon.GraphQL.Resolvers.Participant do def send_private_messages_to_participants(_parent, _args, _resolution), do: {:error, :unauthorized} - defp conversation_to_view(%Conversation{} = conversation, %Actor{} = actor) do + defp conversation_to_view( + %Conversation{id: conversation_id} = conversation, + %Actor{id: actor_id} = actor + ) do value = conversation |> Map.from_struct() |> Map.put(:actor, actor) + |> Map.put(:unread, false) + |> Map.put( + :conversation_participant_id, + Conversations.get_participant_by_conversation_and_actor(conversation_id, actor_id).id + ) struct(ConversationView, value) end diff --git a/lib/mobilizon/discussions/comment.ex b/lib/mobilizon/discussions/comment.ex index 0bd20f24..151f4521 100644 --- a/lib/mobilizon/discussions/comment.ex +++ b/lib/mobilizon/discussions/comment.ex @@ -77,7 +77,7 @@ defmodule Mobilizon.Discussions.Comment do belongs_to(:conversation, Conversation) has_many(:replies, Comment, foreign_key: :origin_comment_id) many_to_many(:tags, Tag, join_through: "comments_tags", on_replace: :delete) - has_many(:mentions, Mention) + has_many(:mentions, Mention, on_replace: :delete) many_to_many(:media, Media, join_through: "comments_medias", on_replace: :delete) timestamps(type: :utc_datetime) diff --git a/lib/service/activity/conversation.ex b/lib/service/activity/conversation.ex index f9dfe973..3f31f1db 100644 --- a/lib/service/activity/conversation.ex +++ b/lib/service/activity/conversation.ex @@ -79,11 +79,16 @@ defmodule Mobilizon.Service.Activity.Conversation do defp send_participant_notifications(_, _, _, _), do: {:ok, :skipped} defp event_subject_params(%Conversation{ - event: %Event{id: conversation_event_id, title: conversation_event_title} + event: %Event{ + id: conversation_event_id, + title: conversation_event_title, + uuid: conversation_event_uuid + } }), do: %{ conversation_event_id: conversation_event_id, - conversation_event_title: conversation_event_title + conversation_event_title: conversation_event_title, + conversation_event_uuid: conversation_event_uuid } defp event_subject_params(_), do: %{} diff --git a/lib/service/formatter/html.ex b/lib/service/formatter/html.ex index 5544eede..67948d03 100644 --- a/lib/service/formatter/html.ex +++ b/lib/service/formatter/html.ex @@ -14,6 +14,8 @@ defmodule Mobilizon.Service.Formatter.HTML do def filter_tags(html), do: Sanitizer.scrub(html, DefaultScrubbler) + defdelegate basic_html(html), to: FastSanitize + @spec strip_tags(String.t()) :: String.t() | no_return() def strip_tags(html) do case FastSanitize.strip_tags(html) do @@ -39,5 +41,17 @@ defmodule Mobilizon.Service.Formatter.HTML do def strip_tags_and_insert_spaces(html), do: html + @spec html_to_text(String.t()) :: String.t() + def html_to_text(html) do + html + |> String.replace(~r/
  • /, "\\g{1}- ", global: true) + |> String.replace( + ~r/<\/?\s?br>|<\/\s?p>|<\/\s?li>|<\/\s?div>|<\/\s?h.>/, + "\\g{1}\n\r", + global: true + ) + |> strip_tags() + end + def filter_tags_for_oembed(html), do: Sanitizer.scrub(html, OEmbed) end diff --git a/lib/service/formatter/text.ex b/lib/service/formatter/text.ex new file mode 100644 index 00000000..140502f1 --- /dev/null +++ b/lib/service/formatter/text.ex @@ -0,0 +1,37 @@ +defmodule Mobilizon.Service.Formatter.Text do + @moduledoc """ + Helps to format text blocks + + Inspired from https://elixirforum.com/t/is-there-are-text-wrapping-library-for-elixir/21733/4 + Using the Knuth-Plass Line Wrapping Algorithm https://www.students.cs.ubc.ca/~cs-490/2015W2/lectures/Knuth.pdf + """ + + def quote_paragraph(string, max_line_length) do + paragraph(string, max_line_length, "> ") + end + + def paragraph(string, max_line_length, prefix \\ "") do + string + |> String.split("\n\n", trim: true) + |> Enum.map(&subparagraph(&1, max_line_length, prefix)) + |> Enum.join("\n#{prefix}\n") + end + + defp subparagraph(string, max_line_length, prefix) do + [word | rest] = String.split(string, ~r/\s+/, trim: true) + + lines_assemble(rest, max_line_length - String.length(prefix), String.length(word), word, []) + |> Enum.map(&"#{prefix}#{&1}") + |> Enum.join("\n") + end + + defp lines_assemble([], _, _, line, acc), do: [line | acc] |> Enum.reverse() + + defp lines_assemble([word | rest], max, line_length, line, acc) do + if line_length + 1 + String.length(word) > max do + lines_assemble(rest, max, String.length(word), word, [line | acc]) + else + lines_assemble(rest, max, line_length + 1 + String.length(word), line <> " " <> word, acc) + end + end +end diff --git a/lib/service/workers/legacy_notifier_builder.ex b/lib/service/workers/legacy_notifier_builder.ex index 4c0f776c..a00e1926 100644 --- a/lib/service/workers/legacy_notifier_builder.ex +++ b/lib/service/workers/legacy_notifier_builder.ex @@ -22,6 +22,13 @@ defmodule Mobilizon.Service.Workers.LegacyNotifierBuilder do notify_anonymous_participants(get_in(args, ["subject_params", "event_id"]), activity) end + if args["subject"] == "conversation_created" do + notify_anonymous_participants( + get_in(args, ["subject_params", "conversation_event_id"]), + activity + ) + end + args |> users_to_notify(author_id: args["author_id"], group_id: Map.get(args, "group_id")) |> Enum.each(¬ify_user(&1, activity)) diff --git a/lib/web/email/activity.ex b/lib/web/email/activity.ex index 4f3f1a9f..686360e0 100644 --- a/lib/web/email/activity.ex +++ b/lib/web/email/activity.ex @@ -10,6 +10,7 @@ defmodule Mobilizon.Web.Email.Activity do alias Mobilizon.Actors.Actor alias Mobilizon.Config alias Mobilizon.Web.Email + require Logger @spec direct_activity(String.t(), list(), Keyword.t()) :: Swoosh.Email.t() def direct_activity( @@ -39,6 +40,36 @@ defmodule Mobilizon.Web.Email.Activity do end @spec anonymous_activity(String.t(), Activity.t(), Keyword.t()) :: Swoosh.Email.t() + def anonymous_activity( + email, + %Activity{subject_params: subject_params, type: :conversation} = activity, + options + ) do + locale = Keyword.get(options, :locale, "en") + + subject = + dgettext( + "activity", + "Informations about your event %{event}", + event: subject_params["conversation_event_title"] + ) + + conversation = Mobilizon.Conversations.get_conversation(activity.object_id) + + Logger.debug("Going to send anonymous activity of type #{activity.type} to #{email}") + + [to: email, subject: subject] + |> Email.base_email() + |> render_body(:email_anonymous_activity, %{ + subject: subject, + activity: activity, + locale: locale, + extra: %{ + "conversation" => conversation + } + }) + end + def anonymous_activity(email, %Activity{subject_params: subject_params} = activity, options) do locale = Keyword.get(options, :locale, "en") @@ -49,6 +80,8 @@ defmodule Mobilizon.Web.Email.Activity do event: subject_params["event_title"] ) + Logger.debug("Going to send anonymous activity of type #{activity.type} to #{email}") + [to: email, subject: subject] |> Email.base_email() |> render_body(:email_anonymous_activity, %{ diff --git a/lib/web/templates/email/email_anonymous_activity.html.heex b/lib/web/templates/email/email_anonymous_activity.html.heex index 4e0d9421..b08a0652 100644 --- a/lib/web/templates/email/email_anonymous_activity.html.heex +++ b/lib/web/templates/email/email_anonymous_activity.html.heex @@ -35,61 +35,164 @@ - - - - + +
    - - - + +
    - <%= dgettext( - "activity", - "%{profile} has posted an announcement under event %{event}.", - %{ - profile: "#{escape_html(display_name_and_username(@activity.author))}", - event: - " URI.decode()}\"> + <%= case @activity.type do %> + <% :comment -> %> + + + + - - - + +
    + + + - -
    + <%= dgettext( + "activity", + "%{profile} has posted a public announcement under event %{event}.", + %{ + profile: + "#{escape_html(display_name_and_username(@activity.author))}", + event: + " URI.decode()}\"> #{escape_html(@activity.subject_params["event_title"])} " - } - ) - |> raw %> -
    -
    - - - + + + - -
    - - - + +
    - raw %> +
    +
    + + + - -
    + + + - -
    + - <%= gettext("Visit event page") %> - -
    -
    -
    + target="_blank" + style="font-size: 20px; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; padding: 15px 25px; border-radius: 2px; border: 1px solid #3C376E; display: inline-block;" + > + <%= gettext("Visit event page") %> + +
    +
    +
    + <% :conversation -> %> + + + + + + + + + + + + + + +
    + + + + +
    + <%= dgettext( + "activity", + "%{profile} has posted a private announcement about event %{event}.", + %{ + profile: + "#{escape_html(display_name_and_username(@activity.author))}", + event: + " URI.decode()}\"> + #{escape_html(@activity.subject_params["conversation_event_title"])} + " + } + ) + |> raw %> + <%= dgettext( + "activity", + "It might give details on how to join the event, so make sure to read it appropriately." + ) %> +
    +
    + + + + +
    + + + + +
    +
    + <%= @extra["conversation"].last_comment.text + |> sanitize_to_basic_html() + |> raw() %> +
    +
    +
    +
    + + + + +
    + <%= dgettext( + "activity", + "This information is sent privately to you as a person who registered for this event. Share the informations above with other people with caution." + ) %> +
    +
    + + + + +
    + + + + +
    + + <%= gettext("Visit event page") %> + +
    +
    +
    + <% end %>