Merge branch 'fixes' into 'main'

Various fixes

Closes #1384, #1382 et #1413

See merge request framasoft/mobilizon!1523
This commit is contained in:
Thomas Citharel 2024-02-08 16:52:15 +00:00
commit 5429afba21
63 changed files with 3168 additions and 1306 deletions

View File

@ -108,9 +108,10 @@
{Credo.Check.Refactor.MatchInCondition, []}, {Credo.Check.Refactor.MatchInCondition, []},
{Credo.Check.Refactor.NegatedConditionsInUnless, []}, {Credo.Check.Refactor.NegatedConditionsInUnless, []},
{Credo.Check.Refactor.NegatedConditionsWithElse, []}, {Credo.Check.Refactor.NegatedConditionsWithElse, []},
{Credo.Check.Refactor.Nesting, [ {Credo.Check.Refactor.Nesting,
max_nesting: 3 [
]}, max_nesting: 3
]},
{Credo.Check.Refactor.PipeChainStart, {Credo.Check.Refactor.PipeChainStart,
[ [
excluded_argument_types: [:atom, :binary, :fn, :keyword, :number], excluded_argument_types: [:atom, :binary, :fn, :keyword, :number],
@ -159,8 +160,7 @@
# Removed checks # Removed checks
# #
{Credo.Check.Warning.LazyLogging, false}, {Credo.Check.Warning.LazyLogging, false},
{Credo.Check.Refactor.MapInto, false}, {Credo.Check.Refactor.MapInto, false}
{Credo.Check.Warning.MissedMetadataKeyInLoggerConfig, false}
] ]
} }
] ]

View File

@ -1,3 +1 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npm run pre-commit npm run pre-commit

View File

@ -36,7 +36,7 @@ config :mobilizon, :instance,
unconfirmed_user_grace_period_hours: 48, unconfirmed_user_grace_period_hours: 48,
activity_expire_days: 365, activity_expire_days: 365,
activity_keep_number: 100, activity_keep_number: 100,
enable_instance_feeds: false, enable_instance_feeds: true,
email_from: "noreply@localhost", email_from: "noreply@localhost",
email_reply_to: "noreply@localhost" email_reply_to: "noreply@localhost"

View File

@ -1,6 +1,7 @@
# Mobilizon instance configuration # Mobilizon instance configuration
import Config import Config
import Mobilizon.Service.Config.Helpers
{:ok, _} = Application.ensure_all_started(:tls_certificate_check) {:ok, _} = Application.ensure_all_started(:tls_certificate_check)
@ -49,9 +50,20 @@ config :mobilizon, :instance,
description: "Change this to a proper description of your instance", description: "Change this to a proper description of your instance",
hostname: System.get_env("MOBILIZON_INSTANCE_HOST", "mobilizon.lan"), hostname: System.get_env("MOBILIZON_INSTANCE_HOST", "mobilizon.lan"),
registrations_open: System.get_env("MOBILIZON_INSTANCE_REGISTRATIONS_OPEN", "false") == "true", registrations_open: System.get_env("MOBILIZON_INSTANCE_REGISTRATIONS_OPEN", "false") == "true",
demo: false, registration_email_allowlist:
allow_relay: true, System.get_env("MOBILIZON_INSTANCE_REGISTRATIONS_EMAIL_ALLOWLIST", "")
federating: true, |> String.split(",", trim: true),
registration_email_denylist:
System.get_env("MOBILIZON_INSTANCE_REGISTRATIONS_EMAIL_DENYLIST", "")
|> String.split(",", trim: true),
disable_database_login:
System.get_env("MOBILIZON_INSTANCE_DISABLE_DATABASE_LOGIN", "false") == "true",
default_language: System.get_env("MOBILIZON_INSTANCE_DEFAULT_LANGUAGE", "en"),
demo: System.get_env("MOBILIZON_INSTANCE_DEMO", "false") == "true",
allow_relay: System.get_env("MOBILIZON_INSTANCE_ALLOW_RELAY", "true") == "true",
federating: System.get_env("MOBILIZON_INSTANCE_FEDERATING", "true") == "true",
enable_instance_feeds:
System.get_env("MOBILIZON_INSTANCE_ENABLE_INSTANCE_FEEDS", "true") == "true",
email_from: System.get_env("MOBILIZON_INSTANCE_EMAIL", "noreply@mobilizon.lan"), email_from: System.get_env("MOBILIZON_INSTANCE_EMAIL", "noreply@mobilizon.lan"),
email_reply_to: System.get_env("MOBILIZON_REPLY_EMAIL", "noreply@mobilizon.lan") email_reply_to: System.get_env("MOBILIZON_REPLY_EMAIL", "noreply@mobilizon.lan")
@ -79,7 +91,7 @@ config :mobilizon, Mobilizon.Web.Email.Mailer,
ssl: System.get_env("MOBILIZON_SMTP_SSL", "false"), ssl: System.get_env("MOBILIZON_SMTP_SSL", "false"),
retries: 1, retries: 1,
no_mx_lookups: false, no_mx_lookups: false,
auth: :if_available auth: System.get_env("MOBILIZON_SMTP_AUTH", "if_available")
config :geolix, config :geolix,
databases: [ databases: [
@ -93,13 +105,30 @@ config :geolix,
config :mobilizon, Mobilizon.Web.Upload.Uploader.Local, config :mobilizon, Mobilizon.Web.Upload.Uploader.Local,
uploads: System.get_env("MOBILIZON_UPLOADS", "/var/lib/mobilizon/uploads") uploads: System.get_env("MOBILIZON_UPLOADS", "/var/lib/mobilizon/uploads")
formats =
if System.get_env("MOBILIZON_EXPORTS_FORMAT_CSV_ENABLED", "true") == "true" do
[Mobilizon.Service.Export.Participants.CSV]
else
[]
end
formats =
if System.get_env("MOBILIZON_EXPORTS_FORMAT_PDF_ENABLED", "true") == "true" do
formats ++ [Mobilizon.Service.Export.Participants.PDF]
else
formats
end
formats =
if System.get_env("MOBILIZON_EXPORTS_FORMAT_ODS_ENABLED", "true") == "true" do
formats ++ [Mobilizon.Service.Export.Participants.ODS]
else
formats
end
config :mobilizon, :exports, config :mobilizon, :exports,
path: System.get_env("MOBILIZON_UPLOADS_EXPORTS", "/var/lib/mobilizon/uploads/exports"), path: System.get_env("MOBILIZON_UPLOADS_EXPORTS", "/var/lib/mobilizon/uploads/exports"),
formats: [ formats: formats
Mobilizon.Service.Export.Participants.CSV,
Mobilizon.Service.Export.Participants.PDF,
Mobilizon.Service.Export.Participants.ODS
]
config :tz_world, config :tz_world,
data_dir: System.get_env("MOBILIZON_TIMEZONES_DIR", "/var/lib/mobilizon/timezones") data_dir: System.get_env("MOBILIZON_TIMEZONES_DIR", "/var/lib/mobilizon/timezones")
@ -110,3 +139,131 @@ config :web_push_encryption, :vapid_details,
subject: System.get_env("MOBILIZON_WEB_PUSH_ENCRYPTION_SUBJECT", nil), subject: System.get_env("MOBILIZON_WEB_PUSH_ENCRYPTION_SUBJECT", nil),
public_key: System.get_env("MOBILIZON_WEB_PUSH_ENCRYPTION_PUBLIC_KEY", nil), public_key: System.get_env("MOBILIZON_WEB_PUSH_ENCRYPTION_PUBLIC_KEY", nil),
private_key: System.get_env("MOBILIZON_WEB_PUSH_ENCRYPTION_PRIVATE_KEY", nil) private_key: System.get_env("MOBILIZON_WEB_PUSH_ENCRYPTION_PRIVATE_KEY", nil)
geospatial_service =
case System.get_env("MOBILIZON_GEOSPATIAL_SERVICE", "Nominatim") do
"Nominatim" -> Mobilizon.Service.Geospatial.Nominatim
"Addok" -> Mobilizon.Service.Geospatial.Addok
"Photon" -> Mobilizon.Service.Geospatial.Photon
"GoogleMaps" -> Mobilizon.Service.Geospatial.GoogleMaps
"MapQuest" -> Mobilizon.Service.Geospatial.MapQuest
"Mimirsbrunn" -> Mobilizon.Service.Geospatial.Mimirsbrunn
"Pelias" -> Mobilizon.Service.Geospatial.Pelias
end
config :mobilizon, Mobilizon.Service.Geospatial, service: geospatial_service
if System.get_env("MOBILIZON_GEOSPATIAL_SERVICE", "Nominatim") == "Nominatim" do
config :mobilizon, Mobilizon.Service.Geospatial.Nominatim,
endpoint:
System.get_env(
"MOBILIZON_GEOSPATIAL_NOMINATIM_ENDPOINT",
"https://nominatim.openstreetmap.org"
),
api_key: System.get_env("MOBILIZON_GEOSPATIAL_NOMINATIM_API_KEY", nil)
end
if System.get_env("MOBILIZON_GEOSPATIAL_SERVICE", "Nominatim") == "Addok" do
config :mobilizon, Mobilizon.Service.Geospatial.Addok,
endpoint:
System.get_env("MOBILIZON_GEOSPATIAL_ADDOK_ENDPOINT", "https://api-adresse.data.gouv.fr")
end
if System.get_env("MOBILIZON_GEOSPATIAL_SERVICE", "Nominatim") == "Photon" do
config :mobilizon, Mobilizon.Service.Geospatial.Photon,
endpoint: System.get_env("MOBILIZON_GEOSPATIAL_PHOTON_ENDPOINT", "https://photon.komoot.de")
end
if System.get_env("MOBILIZON_GEOSPATIAL_SERVICE", "Nominatim") == "GoogleMaps" do
config :mobilizon, Mobilizon.Service.Geospatial.GoogleMaps,
api_key: System.get_env("MOBILIZON_GEOSPATIAL_GOOGLE_MAPS_API_KEY", nil),
fetch_place_details: true
end
if System.get_env("MOBILIZON_GEOSPATIAL_SERVICE", "Nominatim") == "MapQuest" do
config :mobilizon, Mobilizon.Service.Geospatial.MapQuest,
api_key: System.get_env("MOBILIZON_GEOSPATIAL_MAP_QUEST_API_KEY", nil)
end
if System.get_env("MOBILIZON_GEOSPATIAL_SERVICE", "Nominatim") == "Mimirsbrunn" do
config :mobilizon, Mobilizon.Service.Geospatial.Mimirsbrunn,
endpoint: System.get_env("MOBILIZON_GEOSPATIAL_MIMIRSBRUNN_ENDPOINT", nil)
end
if System.get_env("MOBILIZON_GEOSPATIAL_SERVICE", "Nominatim") == "Pelias" do
config :mobilizon, Mobilizon.Service.Geospatial.Pelias,
endpoint: System.get_env("MOBILIZON_GEOSPATIAL_PELIAS_ENDPOINT", nil)
end
sentry_dsn = System.get_env("MOBILIZON_ERROR_REPORTING_SENTRY_DSN", nil)
included_environments = if sentry_dsn, do: ["prod"], else: []
config :sentry,
dsn: sentry_dsn,
included_environments: included_environments,
release: to_string(Application.spec(:mobilizon, :vsn))
config :logger, Sentry.LoggerBackend,
capture_log_messages: true,
level: :error
if sentry_dsn != nil do
config :mobilizon, Mobilizon.Service.ErrorReporting,
adapter: Mobilizon.Service.ErrorReporting.Sentry
end
matomo_enabled = System.get_env("MOBILIZON_FRONT_END_ANALYTICS_MATOMO_ENABLED", "false") == "true"
matomo_endpoint = System.get_env("MOBILIZON_FRONT_END_ANALYTICS_MATOMO_ENDPOINT", nil)
matomo_site_id = System.get_env("MOBILIZON_FRONT_END_ANALYTICS_MATOMO_SITE_ID", nil)
matomo_tracker_file_name =
System.get_env("MOBILIZON_FRONT_END_ANALYTICS_MATOMO_TRACKER_FILE_NAME", "matomo")
matomo_host = host_from_uri(matomo_endpoint)
analytics_providers =
if matomo_enabled do
[Mobilizon.Service.FrontEndAnalytics.Matomo]
else
[]
end
analytics_providers =
if sentry_dsn != nil do
analytics_providers ++ [Mobilizon.Service.FrontEndAnalytics.Sentry]
else
analytics_providers
end
config :mobilizon, :analytics, providers: analytics_providers
matomo_csp =
if matomo_enabled and matomo_host do
[
connect_src: [matomo_host],
script_src: [matomo_host],
img_src: [matomo_host]
]
else
[]
end
config :mobilizon, Mobilizon.Service.FrontEndAnalytics.Matomo,
enabled: matomo_enabled,
host: matomo_endpoint,
siteId: matomo_site_id,
trackerFileName: matomo_tracker_file_name,
csp: matomo_csp
config :mobilizon, Mobilizon.Service.FrontEndAnalytics.Sentry,
enabled: sentry_dsn != nil,
dsn: sentry_dsn,
tracesSampleRate: 1.0,
organization: System.get_env("MOBILIZON_ERROR_REPORTING_SENTRY_ORGANISATION", nil),
project: System.get_env("MOBILIZON_ERROR_REPORTING_SENTRY_PROJECT", nil),
host: System.get_env("MOBILIZON_ERROR_REPORTING_SENTRY_HOST", nil),
csp: [
connect_src:
System.get_env("MOBILIZON_ERROR_REPORTING_SENTRY_HOST", "") |> String.split(" ", trim: true)
]

View File

@ -71,8 +71,10 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
case Discussions.get_comment_from_url_with_preload(object["id"]) do case Discussions.get_comment_from_url_with_preload(object["id"]) do
{:error, :comment_not_found} -> {:error, :comment_not_found} ->
case Converter.Comment.as_to_model_data(object) do case Converter.Comment.as_to_model_data(object) do
%{visibility: visibility, attributed_to_id: attributed_to_id} = object_data %{visibility: visibility, attributed_to_id: attributed_to_id, actor_id: actor_id} =
when visibility === :private and is_nil(attributed_to_id) -> object_data
when visibility === :private and
(is_nil(attributed_to_id) or actor_id == attributed_to_id) ->
Actions.Create.create(:conversation, object_data, false) Actions.Create.create(:conversation, object_data, false)
object_data when is_map(object_data) -> object_data when is_map(object_data) ->

View File

@ -6,10 +6,13 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
internal one, and back. internal one, and back.
""" """
alias Cldr.DateTime.Formatter
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Addresses.Address alias Mobilizon.Addresses.Address
alias Mobilizon.Events.Categories alias Mobilizon.Events.Categories
alias Mobilizon.Events.Event, as: EventModel alias Mobilizon.Events.Event, as: EventModel
alias Mobilizon.Events.EventOptions
alias Mobilizon.Medias.Media alias Mobilizon.Medias.Media
alias Mobilizon.Federation.ActivityStream.{Converter, Convertible} alias Mobilizon.Federation.ActivityStream.{Converter, Convertible}
@ -29,7 +32,14 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
maybe_fetch_actor_and_attributed_to_id: 1, maybe_fetch_actor_and_attributed_to_id: 1,
process_pictures: 2, process_pictures: 2,
get_address: 1, get_address: 1,
fetch_actor: 1 fetch_actor: 1,
visibility_public?: 1
]
import Mobilizon.Service.Metadata.Utils,
only: [
datetime_to_string: 3,
render_address!: 1
] ]
require Logger require Logger
@ -146,7 +156,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
"anonymousParticipationEnabled" => event.options.anonymous_participation, "anonymousParticipationEnabled" => event.options.anonymous_participation,
"attachment" => Enum.map(event.metadata, &EventMetadataConverter.metadata_to_as/1), "attachment" => Enum.map(event.metadata, &EventMetadataConverter.metadata_to_as/1),
"draft" => event.draft, "draft" => event.draft,
# Remove me in MBZ 5.x # TODO: Remove me in MBZ 5.x
"ical:status" => event.status |> to_string |> String.upcase(), "ical:status" => event.status |> to_string |> String.upcase(),
"status" => event.status |> to_string |> String.upcase(), "status" => event.status |> to_string |> String.upcase(),
"id" => event.url, "id" => event.url,
@ -154,7 +164,8 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
"inLanguage" => event.language, "inLanguage" => event.language,
"timezone" => event.options.timezone, "timezone" => event.options.timezone,
"contacts" => Enum.map(event.contacts, & &1.url), "contacts" => Enum.map(event.contacts, & &1.url),
"isOnline" => event.options.is_online "isOnline" => event.options.is_online,
"summary" => event_summary(event)
} }
|> maybe_add_physical_address(event) |> maybe_add_physical_address(event)
|> maybe_add_event_picture(event) |> maybe_add_event_picture(event)
@ -216,7 +227,8 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
defp get_metdata(_), do: [] defp get_metdata(_), do: []
defp get_visibility(object), do: if(@ap_public in object["to"], do: :public, else: :unlisted) defp get_visibility(object),
do: if(visibility_public?(object["to"]), do: :public, else: :unlisted)
@spec date_to_string(DateTime.t() | nil) :: String.t() @spec date_to_string(DateTime.t() | nil) :: String.t()
defp date_to_string(nil), do: nil defp date_to_string(nil), do: nil
@ -341,4 +353,47 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
_participant_count _participant_count
), ),
do: nil do: nil
def event_summary(%EventModel{
begins_on: begins_on,
physical_address: address,
options: %EventOptions{timezone: timezone},
language: language
}) do
begins_on = build_begins_on(begins_on, timezone)
begins_on
|> datetime_to_string(language || "en", :long)
|> (&[&1]).()
|> add_timezone(begins_on)
|> maybe_build_address(address)
|> Enum.join(" - ")
end
@spec build_begins_on(DateTime.t(), String.t() | nil) :: DateTime.t()
defp build_begins_on(begins_on, nil), do: begins_on
defp build_begins_on(begins_on, timezone) do
case DateTime.shift_zone(begins_on, timezone) do
{:ok, begins_on} -> begins_on
{:error, _err} -> begins_on
end
end
defp add_timezone(elements, %DateTime{} = begins_on) do
elements ++ [Formatter.zone_gmt(begins_on)]
end
@spec maybe_build_address(list(String.t()), Address.t() | nil) :: list(String.t())
defp maybe_build_address(elements, %Address{} = address) do
elements ++ [render_address!(address)]
rescue
# If the address is not renderable
e in ArgumentError ->
require Logger
Logger.error(Exception.format(:error, e, __STACKTRACE__))
elements
end
defp maybe_build_address(elements, _address), do: elements
end end

View File

@ -15,7 +15,8 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Post do
import Mobilizon.Federation.ActivityStream.Converter.Utils, import Mobilizon.Federation.ActivityStream.Converter.Utils,
only: [ only: [
process_pictures: 2 process_pictures: 2,
visibility_public?: 1
] ]
import Mobilizon.Service.Guards, only: [is_valid_string: 1] import Mobilizon.Service.Guards, only: [is_valid_string: 1]
@ -134,14 +135,12 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Post do
) )
end end
@ap_public "https://www.w3.org/ns/activitystreams#Public"
defp get_visibility(%{"to" => to}, %Actor{ defp get_visibility(%{"to" => to}, %Actor{
followers_url: followers_url, followers_url: followers_url,
members_url: members_url members_url: members_url
}) do }) do
cond do cond do
@ap_public in to -> :public visibility_public?(to) -> :public
followers_url in to -> :unlisted followers_url in to -> :unlisted
members_url in to -> :private members_url in to -> :private
end end

View File

@ -335,4 +335,13 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Utils do
nil nil
end end
end end
@ap_public "https://www.w3.org/ns/activitystreams#Public"
@spec visibility_public?(String.t() | list(String.t())) :: boolean()
def visibility_public?(to) when is_binary(to), do: visibility_public?([to])
def visibility_public?(to) when is_list(to) do
!MapSet.disjoint?(MapSet.new(to), MapSet.new([@ap_public, "as:Public", "Public"]))
end
end end

View File

@ -156,6 +156,9 @@ defmodule Mobilizon.GraphQL.Resolvers.Conversation do
{:ok, conversation_to_view(conversation, conversation_participant_actor)} {:ok, conversation_to_view(conversation, conversation_participant_actor)}
{:error, %Ecto.Changeset{} = changeset} ->
{:error, changeset}
{:error, :empty_participants} -> {:error, :empty_participants} ->
{:error, {:error,
dgettext( dgettext(

View File

@ -381,10 +381,15 @@ defmodule Mobilizon.GraphQL.Resolvers.Participant do
visibility: :private visibility: :private
}) })
with {:member, true} <- with {:ok,
%Event{organizer_actor_id: organizer_actor_id, attributed_to_id: attributed_to_id} =
_event} <- Mobilizon.Events.get_event(event_id),
{:member, true} <-
{:member, {:member,
to_string(current_actor_id) == to_string(actor_id) or (to_string(current_actor_id) == to_string(organizer_actor_id) and
Actors.member?(current_actor_id, actor_id)}, to_string(current_actor_id) == to_string(actor_id)) or
(!is_nil(attributed_to_id) and Actors.member?(current_actor_id, attributed_to_id) and
to_string(attributed_to_id) == to_string(actor_id))},
{:ok, _activity, %Conversation{} = conversation} <- Comments.create_conversation(args) do {:ok, _activity, %Conversation{} = conversation} <- Comments.create_conversation(args) do
{:ok, conversation_to_view(conversation, Actors.get_actor(actor_id))} {:ok, conversation_to_view(conversation, Actors.get_actor(actor_id))}
else else

View File

@ -20,6 +20,7 @@ defmodule Mobilizon.GraphQL.Schema.ConversationType do
) )
field(:last_comment, :comment, description: "The last comment of the conversation") field(:last_comment, :comment, description: "The last comment of the conversation")
field(:origin_comment, :comment, description: "The first comment of the conversation")
field :comments, :paginated_comment_list do field :comments, :paginated_comment_list do
arg(:page, :integer, default_value: 1) arg(:page, :integer, default_value: 1)

View File

@ -4,7 +4,7 @@ defmodule Mobilizon.GraphQL.Schema.Discussions.CommentType do
""" """
use Absinthe.Schema.Notation use Absinthe.Schema.Notation
import Absinthe.Resolution.Helpers, only: [dataloader: 1] import Absinthe.Resolution.Helpers, only: [dataloader: 1, dataloader: 2]
alias Mobilizon.{Actors, Discussions, Events} alias Mobilizon.{Actors, Discussions, Events}
alias Mobilizon.GraphQL.Resolvers.Comment alias Mobilizon.GraphQL.Resolvers.Comment
@ -23,7 +23,7 @@ defmodule Mobilizon.GraphQL.Schema.Discussions.CommentType do
field(:replies, list_of(:comment)) do field(:replies, list_of(:comment)) do
description("A list of replies to the comment") description("A list of replies to the comment")
resolve(dataloader(Discussions)) resolve(dataloader(Discussions, args: %{replies: true}))
end end
field(:total_replies, :integer, field(:total_replies, :integer,
@ -47,6 +47,12 @@ defmodule Mobilizon.GraphQL.Schema.Discussions.CommentType do
field(:threadLanguages, non_null(list_of(:string)), description: "The thread languages") field(:threadLanguages, non_null(list_of(:string)), description: "The thread languages")
field(:actor, :person, resolve: dataloader(Actors), description: "The comment's author") field(:actor, :person, resolve: dataloader(Actors), description: "The comment's author")
field(:attributed_to, :actor,
resolve: dataloader(Actors),
description: "The comment's attributed to actor"
)
field(:inserted_at, :datetime, description: "When was the comment inserted in database") field(:inserted_at, :datetime, description: "When was the comment inserted in database")
field(:updated_at, :datetime, description: "When was the comment updated") field(:updated_at, :datetime, description: "When was the comment updated")
field(:deleted_at, :datetime, description: "When was the comment deleted") field(:deleted_at, :datetime, description: "When was the comment deleted")

View File

@ -104,6 +104,10 @@ defmodule Mobilizon.Conversations do
|> join(:inner, [_cp, _c, _e, _a, _lc, _oc, p], ap in Actor, on: p.actor_id == ap.id) |> join(:inner, [_cp, _c, _e, _a, _lc, _oc, p], ap in Actor, on: p.actor_id == ap.id)
|> where([_cp, c], c.event_id == ^event_id) |> where([_cp, c], c.event_id == ^event_id)
|> where([cp], cp.actor_id == ^actor_id) |> where([cp], cp.actor_id == ^actor_id)
|> where(
[_cp, _c, _e, _a, _lc, oc],
oc.actor_id == ^actor_id or oc.attributed_to_id == ^actor_id
)
|> order_by([cp], desc: cp.unread, desc: cp.updated_at) |> order_by([cp], desc: cp.unread, desc: cp.updated_at)
|> preload([_cp, c, e, a, lc, oc, p, ap], |> preload([_cp, c, e, a, lc, oc, p, ap],
actor: a, actor: a,
@ -113,6 +117,14 @@ defmodule Mobilizon.Conversations do
|> Page.build_page(page, limit) |> Page.build_page(page, limit)
end end
def find_all_conversations_for_event(event_id) do
ConversationParticipant
|> join(:inner, [cp], c in Conversation, on: cp.conversation_id == c.id)
|> join(:left, [_cp, c], e in Event, on: c.event_id == e.id)
|> where([_cp, c], c.event_id == ^event_id)
|> Repo.all()
end
@spec list_conversation_participants_for_actor( @spec list_conversation_participants_for_actor(
integer | String.t(), integer | String.t(),
integer | nil, integer | nil,
@ -133,7 +145,7 @@ defmodule Mobilizon.Conversations do
subquery subquery
|> subquery() |> subquery()
|> order_by([cp], desc: cp.unread, desc: cp.updated_at) |> order_by([cp], desc: cp.unread, desc: cp.updated_at)
|> preload([:actor, conversation: [:last_comment, :participants]]) |> preload([:actor, conversation: [:last_comment, :origin_comment, :participants, :event]])
|> Page.build_page(page, limit) |> Page.build_page(page, limit)
end end
@ -147,7 +159,7 @@ defmodule Mobilizon.Conversations do
ConversationParticipant ConversationParticipant
|> join(:inner, [cp], a in Actor, on: cp.actor_id == a.id) |> join(:inner, [cp], a in Actor, on: cp.actor_id == a.id)
|> where([_cp, a], a.user_id == ^user_id) |> where([_cp, a], a.user_id == ^user_id)
|> preload([:actor, conversation: [:last_comment, :participants]]) |> preload([:actor, conversation: [:last_comment, :origin_comment, :participants, :event]])
|> Page.build_page(page, limit) |> Page.build_page(page, limit)
end end

View File

@ -85,6 +85,13 @@ defmodule Mobilizon.Discussions do
|> select([c, r], %{c | total_replies: count(r.id)}) |> select([c, r], %{c | total_replies: count(r.id)})
end end
# Replies are only used on event comments, so we always use public visibily here
def query(Comment, %{replies: true}) do
Comment
|> where([c], c.visibility in ^@public_visibility)
|> order_by([c], asc: :is_announcement, asc: :published_at)
end
def query(Comment, _) do def query(Comment, _) do
order_by(Comment, [c], asc: :is_announcement, asc: :published_at) order_by(Comment, [c], asc: :is_announcement, asc: :published_at)
end end

View File

@ -792,7 +792,7 @@ defmodule Mobilizon.Events do
end end
end end
def get_participant(event_id, actor_id, %{}) do def get_participant(event_id, actor_id, _params) do
case Participant case Participant
|> Repo.get_by(event_id: event_id, actor_id: actor_id) |> Repo.get_by(event_id: event_id, actor_id: actor_id)
|> Repo.preload(@participant_preloads) do |> Repo.preload(@participant_preloads) do

View File

@ -2,7 +2,7 @@ defmodule Mobilizon.Service.Activity.Conversation do
@moduledoc """ @moduledoc """
Insert a conversation activity Insert a conversation activity
""" """
alias Mobilizon.Conversations alias Mobilizon.{Actors, Conversations}
alias Mobilizon.Conversations.{Conversation, ConversationParticipant} alias Mobilizon.Conversations.{Conversation, ConversationParticipant}
alias Mobilizon.Discussions.Comment alias Mobilizon.Discussions.Comment
alias Mobilizon.Events.Event alias Mobilizon.Events.Event
@ -38,7 +38,7 @@ defmodule Mobilizon.Service.Activity.Conversation do
%Conversation{ %Conversation{
id: conversation_id id: conversation_id
} = conversation, } = conversation,
%Comment{actor_id: actor_id, text: last_comment_text}, %Comment{actor_id: actor_id, text: last_comment_text} = comment,
_options _options
) )
when subject in [ when subject in [
@ -55,7 +55,8 @@ defmodule Mobilizon.Service.Activity.Conversation do
actor_id: conversation_participant_actor_id actor_id: conversation_participant_actor_id
} = } =
conversation_participant -> conversation_participant ->
if actor_id != conversation_participant_actor_id do if actor_id != conversation_participant_actor_id and
can_send_event_announcement?(conversation, comment) do
LegacyNotifierBuilder.enqueue( LegacyNotifierBuilder.enqueue(
:legacy_notify, :legacy_notify,
%{ %{
@ -98,4 +99,31 @@ defmodule Mobilizon.Service.Activity.Conversation do
} }
defp event_subject_params(_), do: %{} defp event_subject_params(_), do: %{}
@spec can_send_event_announcement?(Conversation.t(), Comment.t()) :: boolean()
defp can_send_event_announcement?(
%Conversation{
event: %Event{
attributed_to_id: attributed_to_id
}
},
%Comment{actor_id: actor_id}
)
when not is_nil(attributed_to_id) do
attributed_to_id == actor_id or Actors.member?(actor_id, attributed_to_id)
end
defp can_send_event_announcement?(
%Conversation{
event: %Event{
organizer_actor_id: organizer_actor_id
}
},
%Comment{actor_id: actor_id}
)
when not is_nil(organizer_actor_id) do
organizer_actor_id == actor_id
end
defp can_send_event_announcement?(_, _), do: false
end end

View File

@ -85,7 +85,7 @@ defmodule Mobilizon.Service.Address do
defined?(street) -> defined?(street) ->
if defined?(locality), do: "#{street} (#{locality})", else: street if defined?(locality), do: "#{street} (#{locality})", else: street
defined?(locality) -> defined?(locality) and locality != region ->
"#{locality}, #{region}, #{country}" "#{locality}, #{region}, #{country}"
defined?(region) -> defined?(region) ->

View File

@ -0,0 +1,12 @@
defmodule Mobilizon.Service.Config.Helpers do
@moduledoc """
Provide some helpers to configuration files
"""
@spec host_from_uri(String.t() | nil) :: String.t() | nil
def host_from_uri(nil), do: nil
def host_from_uri(uri) when is_binary(uri) do
URI.parse(uri).host
end
end

View File

@ -136,14 +136,12 @@ defimpl Mobilizon.Service.Metadata, for: Mobilizon.Events.Event do
defp build_language(language, locale), do: language || locale defp build_language(language, locale), do: language || locale
@spec build_begins_on(DateTime.t(), String.t() | nil) :: DateTime.t() @spec build_begins_on(DateTime.t(), String.t() | nil) :: DateTime.t()
defp build_begins_on(begins_on, nil), do: begins_on
defp build_begins_on(begins_on, timezone) do defp build_begins_on(begins_on, timezone) do
if timezone do case DateTime.shift_zone(begins_on, timezone) do
case DateTime.shift_zone(begins_on, timezone) do {:ok, begins_on} -> begins_on
{:ok, begins_on} -> begins_on {:error, _err} -> begins_on
{:error, _err} -> begins_on
end
else
begins_on
end end
end end

View File

@ -56,7 +56,6 @@ defmodule Mobilizon.Service.SiteMap do
end) end)
|> Sitemapper.generate(config) |> Sitemapper.generate(config)
|> Sitemapper.persist(config) |> Sitemapper.persist(config)
|> Sitemapper.ping(config)
|> Stream.run() |> Stream.run()
end, end,
timeout: :infinity timeout: :infinity

View File

@ -4,7 +4,7 @@ defmodule Mobilizon.Service.Workers.LegacyNotifierBuilder do
""" """
alias Mobilizon.Activities.Activity alias Mobilizon.Activities.Activity
alias Mobilizon.{Actors, Events, Users} alias Mobilizon.{Actors, Config, Events, Users}
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Events.{Event, Participant} alias Mobilizon.Events.{Event, Participant}
alias Mobilizon.Service.Notifier alias Mobilizon.Service.Notifier
@ -37,9 +37,10 @@ defmodule Mobilizon.Service.Workers.LegacyNotifierBuilder do
end end
defp special_handling("conversation_created", args, activity) do defp special_handling("conversation_created", args, activity) do
notify_participants( notify_participant(
get_in(args, ["subject_params", "conversation_event_id"]), get_in(args, ["subject_params", "conversation_event_id"]),
activity, activity,
get_in(args, ["participant", "actor_id"]),
args["author_id"] args["author_id"]
) )
end end
@ -143,6 +144,24 @@ defmodule Mobilizon.Service.Workers.LegacyNotifierBuilder do
notify_anonymous_participants(event_id, activity) notify_anonymous_participants(event_id, activity)
end end
defp notify_participant(nil, _activity, _conversation_participant_actor_id, _author_id),
do: :ok
defp notify_participant(event_id, activity, conversation_participant_actor_id, author_id) do
# Anonymous participation
if conversation_participant_actor_id == Config.anonymous_actor_id() do
notify_anonymous_participants(event_id, activity)
else
[conversation_participant_actor_id]
|> users_from_actor_ids(author_id)
|> Enum.each(fn user ->
Notifier.Email.send_anonymous_activity(user.email, activity,
locale: Map.get(user, :locale, "en")
)
end)
end
end
defp notify_anonymous_participants(nil, _activity), do: :ok defp notify_anonymous_participants(nil, _activity), do: :ok
defp notify_anonymous_participants(event_id, activity) do defp notify_anonymous_participants(event_id, activity) do
@ -154,7 +173,7 @@ defmodule Mobilizon.Service.Workers.LegacyNotifierBuilder do
|> Enum.map(fn %Participant{metadata: metadata} -> metadata end) |> Enum.map(fn %Participant{metadata: metadata} -> metadata end)
|> Enum.map(fn %{email: email} = metadata -> |> Enum.map(fn %{email: email} = metadata ->
Notifier.Email.send_anonymous_activity(email, activity, Notifier.Email.send_anonymous_activity(email, activity,
locale: Map.get(metadata, :locale, "en") locale: Map.get(metadata, :locale, "en") || "en"
) )
end) end)
end end

View File

@ -95,7 +95,7 @@ defmodule Mobilizon.Web.Router do
forward("/", Absinthe.Plug, forward("/", Absinthe.Plug,
schema: Mobilizon.GraphQL.Schema, schema: Mobilizon.GraphQL.Schema,
analyze_complexity: true, analyze_complexity: true,
max_complexity: 250 max_complexity: 300
) )
end end

16
mix.exs
View File

@ -121,10 +121,12 @@ defmodule Mobilizon.Mixfile do
|> to_string() |> to_string()
|> String.split() |> String.split()
|> Enum.map(fn strategy_entry -> |> Enum.map(fn strategy_entry ->
with [_strategy, dependency] <- String.split(strategy_entry, ":") do case String.split(strategy_entry, ":") do
dependency [_strategy, dependency] ->
else dependency
[strategy] -> "ueberauth_#{strategy}"
[strategy] ->
"ueberauth_#{strategy}"
end end
end) end)
@ -185,7 +187,7 @@ defmodule Mobilizon.Mixfile do
{:floki, "~> 0.31"}, {:floki, "~> 0.31"},
{:ip_reserved, "~> 0.1.0"}, {:ip_reserved, "~> 0.1.0"},
{:fast_sanitize, "~> 0.1"}, {:fast_sanitize, "~> 0.1"},
{:ueberauth, "0.10.5", override: true}, {:ueberauth, "0.10.7", override: true},
{:ueberauth_twitter, "~> 0.4"}, {:ueberauth_twitter, "~> 0.4"},
{:ueberauth_discord, "~> 0.7"}, {:ueberauth_discord, "~> 0.7"},
{:ueberauth_github, "~> 0.8.1"}, {:ueberauth_github, "~> 0.8.1"},
@ -283,7 +285,7 @@ defmodule Mobilizon.Mixfile do
File.rm_rf!("test/uploads") File.rm_rf!("test/uploads")
end end
defp docs() do defp docs do
[ [
source_ref: "v#{@version}", source_ref: "v#{@version}",
groups_for_modules: groups_for_modules(), groups_for_modules: groups_for_modules(),
@ -323,7 +325,7 @@ defmodule Mobilizon.Mixfile do
] ]
end end
defp groups_for_modules() do defp groups_for_modules do
[ [
Models: [ Models: [
~r/Mobilizon.Actors~r/, ~r/Mobilizon.Actors~r/,

View File

@ -14,10 +14,10 @@
"comeonin": {:hex, :comeonin, "5.4.0", "246a56ca3f41d404380fc6465650ddaa532c7f98be4bda1b4656b3a37cc13abe", [:mix], [], "hexpm", "796393a9e50d01999d56b7b8420ab0481a7538d0caf80919da493b4a6e51faf1"}, "comeonin": {:hex, :comeonin, "5.4.0", "246a56ca3f41d404380fc6465650ddaa532c7f98be4bda1b4656b3a37cc13abe", [:mix], [], "hexpm", "796393a9e50d01999d56b7b8420ab0481a7538d0caf80919da493b4a6e51faf1"},
"connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"}, "connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"},
"cors_plug": {:hex, :cors_plug, "3.0.3", "7c3ac52b39624bc616db2e937c282f3f623f25f8d550068b6710e58d04a0e330", [:mix], [{:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "3f2d759e8c272ed3835fab2ef11b46bddab8c1ab9528167bd463b6452edf830d"}, "cors_plug": {:hex, :cors_plug, "3.0.3", "7c3ac52b39624bc616db2e937c282f3f623f25f8d550068b6710e58d04a0e330", [:mix], [{:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "3f2d759e8c272ed3835fab2ef11b46bddab8c1ab9528167bd463b6452edf830d"},
"cowboy": {:hex, :cowboy, "2.10.0", "ff9ffeff91dae4ae270dd975642997afe2a1179d94b1887863e43f681a203e26", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "3afdccb7183cc6f143cb14d3cf51fa00e53db9ec80cdcd525482f5e99bc41d6b"}, "cowboy": {:hex, :cowboy, "2.11.0", "356bf784599cf6f2cdc6ad12fdcfb8413c2d35dab58404cf000e1feaed3f5645", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "0fa395437f1b0e104e0e00999f39d2ac5f4082ac5049b67a5b6d56ecc31b1403"},
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
"cowlib": {:hex, :cowlib, "2.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"}, "cowlib": {:hex, :cowlib, "2.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"},
"credo": {:hex, :credo, "1.7.2", "fdee3a7cb553d8f2e773569181f0a4a2bb7d192e27e325404cc31b354f59d68c", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "dd15d6fbc280f6cf9b269f41df4e4992dee6615939653b164ef951f60afcb68e"}, "credo": {:hex, :credo, "1.7.3", "05bb11eaf2f2b8db370ecaa6a6bda2ec49b2acd5e0418bc106b73b07128c0436", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "35ea675a094c934c22fb1dca3696f3c31f2728ae6ef5a53b5d648c11180a4535"},
"credo_code_climate": {:hex, :credo_code_climate, "0.1.0", "1c4efbd11cb0244622ed5f09246b9afbbf796316ce03e78f67db6d81271d2978", [:mix], [{:credo, "~> 1.5", [hex: :credo, repo: "hexpm", optional: false]}, {:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "75529fe38056f4e229821d604758282838b8397c82e2c12e409fda16b16821ca"}, "credo_code_climate": {:hex, :credo_code_climate, "0.1.0", "1c4efbd11cb0244622ed5f09246b9afbbf796316ce03e78f67db6d81271d2978", [:mix], [{:credo, "~> 1.5", [hex: :credo, repo: "hexpm", optional: false]}, {:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "75529fe38056f4e229821d604758282838b8397c82e2c12e409fda16b16821ca"},
"dataloader": {:hex, :dataloader, "2.0.0", "49b42d60b9bb06d761a71d7b034c4b34787957e713d4fae15387a25fcd639112", [:mix], [{:ecto, ">= 3.4.3 and < 4.0.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:opentelemetry_process_propagator, "~> 0.2.1", [hex: :opentelemetry_process_propagator, repo: "hexpm", optional: true]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "09d61781b76ce216e395cdbc883ff00d00f46a503e215c22722dba82507dfef0"}, "dataloader": {:hex, :dataloader, "2.0.0", "49b42d60b9bb06d761a71d7b034c4b34787957e713d4fae15387a25fcd639112", [:mix], [{:ecto, ">= 3.4.3 and < 4.0.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:opentelemetry_process_propagator, "~> 0.2.1", [hex: :opentelemetry_process_propagator, repo: "hexpm", optional: true]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "09d61781b76ce216e395cdbc883ff00d00f46a503e215c22722dba82507dfef0"},
"db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"}, "db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"},
@ -34,7 +34,7 @@
"ecto_shortuuid": {:hex, :ecto_shortuuid, "0.2.0", "57cae7b6016cc56a04457b4fc8f63957398dfd9023ff3e900eaf6805a40f8043", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:shortuuid, "~> 2.1 or ~> 3.0", [hex: :shortuuid, repo: "hexpm", optional: false]}], "hexpm", "b92e3b71e86be92f5a7ef6f3de170e7864454e630f7b01dd930414baf38efb65"}, "ecto_shortuuid": {:hex, :ecto_shortuuid, "0.2.0", "57cae7b6016cc56a04457b4fc8f63957398dfd9023ff3e900eaf6805a40f8043", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:shortuuid, "~> 2.1 or ~> 3.0", [hex: :shortuuid, repo: "hexpm", optional: false]}], "hexpm", "b92e3b71e86be92f5a7ef6f3de170e7864454e630f7b01dd930414baf38efb65"},
"ecto_sql": {:hex, :ecto_sql, "3.11.1", "e9abf28ae27ef3916b43545f9578b4750956ccea444853606472089e7d169470", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ce14063ab3514424276e7e360108ad6c2308f6d88164a076aac8a387e1fea634"}, "ecto_sql": {:hex, :ecto_sql, "3.11.1", "e9abf28ae27ef3916b43545f9578b4750956ccea444853606472089e7d169470", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ce14063ab3514424276e7e360108ad6c2308f6d88164a076aac8a387e1fea634"},
"elixir_feed_parser": {:hex, :elixir_feed_parser, "2.1.0", "bb96fb6422158dc7ad59de62ef211cc69d264acbbe63941a64a5dce97bbbc2e6", [:mix], [{:timex, "~> 3.4", [hex: :timex, repo: "hexpm", optional: false]}], "hexpm", "2d3c62fe7b396ee3b73d7160bc8fadbd78bfe9597c98c7d79b3f1038d9cba28f"}, "elixir_feed_parser": {:hex, :elixir_feed_parser, "2.1.0", "bb96fb6422158dc7ad59de62ef211cc69d264acbbe63941a64a5dce97bbbc2e6", [:mix], [{:timex, "~> 3.4", [hex: :timex, repo: "hexpm", optional: false]}], "hexpm", "2d3c62fe7b396ee3b73d7160bc8fadbd78bfe9597c98c7d79b3f1038d9cba28f"},
"elixir_make": {:hex, :elixir_make, "0.7.7", "7128c60c2476019ed978210c245badf08b03dbec4f24d05790ef791da11aa17c", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "5bc19fff950fad52bbe5f211b12db9ec82c6b34a9647da0c2224b8b8464c7e6c"}, "elixir_make": {:hex, :elixir_make, "0.7.8", "505026f266552ee5aabca0b9f9c229cbb496c689537c9f922f3eb5431157efc7", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.0", [hex: :certifi, repo: "hexpm", optional: true]}], "hexpm", "7a71945b913d37ea89b06966e1342c85cfe549b15e6d6d081e8081c493062c07"},
"erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
"erlport": {:hex, :erlport, "0.11.0", "8bb46a520e6eb9146e655fbf9b824433d9d532194667069d9aa45696aae9684b", [:rebar3], [], "hexpm", "8eb136ccaf3948d329b8d1c3278ad2e17e2a7319801bc4cc2da6db278204eee4"}, "erlport": {:hex, :erlport, "0.11.0", "8bb46a520e6eb9146e655fbf9b824433d9d532194667069d9aa45696aae9684b", [:rebar3], [], "hexpm", "8eb136ccaf3948d329b8d1c3278ad2e17e2a7319801bc4cc2da6db278204eee4"},
"eternal": {:hex, :eternal, "1.2.2", "d1641c86368de99375b98d183042dd6c2b234262b8d08dfd72b9eeaafc2a1abd", [:mix], [], "hexpm", "2c9fe32b9c3726703ba5e1d43a1d255a4f3f2d8f8f9bc19f094c7cb1a7a9e782"}, "eternal": {:hex, :eternal, "1.2.2", "d1641c86368de99375b98d183042dd6c2b234262b8d08dfd72b9eeaafc2a1abd", [:mix], [], "hexpm", "2c9fe32b9c3726703ba5e1d43a1d255a4f3f2d8f8f9bc19f094c7cb1a7a9e782"},
@ -43,9 +43,9 @@
"ex_cldr_currencies": {:hex, :ex_cldr_currencies, "2.15.1", "e92ba17c41e7405b7784e0e65f406b5f17cfe313e0e70de9befd653e12854822", [:mix], [{:ex_cldr, "~> 2.34", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "31df8bd37688340f8819bdd770eb17d659652078d34db632b85d4a32864d6a25"}, "ex_cldr_currencies": {:hex, :ex_cldr_currencies, "2.15.1", "e92ba17c41e7405b7784e0e65f406b5f17cfe313e0e70de9befd653e12854822", [:mix], [{:ex_cldr, "~> 2.34", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "31df8bd37688340f8819bdd770eb17d659652078d34db632b85d4a32864d6a25"},
"ex_cldr_dates_times": {:hex, :ex_cldr_dates_times, "2.16.0", "d9848a5de83b6f1bcba151cc43d63b5c6311813cd605b1df1afd896dfdd21001", [:mix], [{:calendar_interval, "~> 0.2", [hex: :calendar_interval, repo: "hexpm", optional: true]}, {:ex_cldr_calendars, "~> 1.22", [hex: :ex_cldr_calendars, repo: "hexpm", optional: false]}, {:ex_cldr_numbers, "~> 2.31", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:tz, "~> 0.26", [hex: :tz, repo: "hexpm", optional: true]}], "hexpm", "0f2f250d479cadda4e0ef3a5e3d936ae7ba1a3f1199db6791e284e86203495b1"}, "ex_cldr_dates_times": {:hex, :ex_cldr_dates_times, "2.16.0", "d9848a5de83b6f1bcba151cc43d63b5c6311813cd605b1df1afd896dfdd21001", [:mix], [{:calendar_interval, "~> 0.2", [hex: :calendar_interval, repo: "hexpm", optional: true]}, {:ex_cldr_calendars, "~> 1.22", [hex: :ex_cldr_calendars, repo: "hexpm", optional: false]}, {:ex_cldr_numbers, "~> 2.31", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:tz, "~> 0.26", [hex: :tz, repo: "hexpm", optional: true]}], "hexpm", "0f2f250d479cadda4e0ef3a5e3d936ae7ba1a3f1199db6791e284e86203495b1"},
"ex_cldr_languages": {:hex, :ex_cldr_languages, "0.3.3", "9787002803552b15a7ade19496c9e46fc921baca992ea80d0394e11fe3acea45", [:mix], [{:ex_cldr, "~> 2.25", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "22fb1fef72b7b4b4872d243b34e7b83734247a78ad87377986bf719089cc447a"}, "ex_cldr_languages": {:hex, :ex_cldr_languages, "0.3.3", "9787002803552b15a7ade19496c9e46fc921baca992ea80d0394e11fe3acea45", [:mix], [{:ex_cldr, "~> 2.25", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "22fb1fef72b7b4b4872d243b34e7b83734247a78ad87377986bf719089cc447a"},
"ex_cldr_numbers": {:hex, :ex_cldr_numbers, "2.32.3", "b631ff94c982ec518e46bf4736000a30a33d6b58facc085d5f240305f512ad4a", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:digital_token, "~> 0.3 or ~> 1.0", [hex: :digital_token, repo: "hexpm", optional: false]}, {:ex_cldr, "~> 2.37", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:ex_cldr_currencies, ">= 2.14.2", [hex: :ex_cldr_currencies, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "7b626ff1e59a0ec9c3c5db5ce9ca91a6995e2ab56426b71f3cbf67181ea225f5"}, "ex_cldr_numbers": {:hex, :ex_cldr_numbers, "2.32.4", "5562148dfc631b04712983975093d2aac29df30b3bf2f7257e0c94b85b72e91b", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:digital_token, "~> 0.3 or ~> 1.0", [hex: :digital_token, repo: "hexpm", optional: false]}, {:ex_cldr, "~> 2.37", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:ex_cldr_currencies, ">= 2.14.2", [hex: :ex_cldr_currencies, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "6fd5a82f0785418fa8b698c0be2b1845dff92b77f1b3172c763d37868fb503d2"},
"ex_cldr_plugs": {:hex, :ex_cldr_plugs, "1.3.1", "ae58748df815ad21b8618830374a28b2ab593230e5df70ed9f647e953a884bec", [:mix], [{:ex_cldr, "~> 2.37", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:gettext, "~> 0.19", [hex: :gettext, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "4f7b4a5fe061734cef7b62ff29118ed6ac72698cdd7bcfc97495db73611fe0fe"}, "ex_cldr_plugs": {:hex, :ex_cldr_plugs, "1.3.1", "ae58748df815ad21b8618830374a28b2ab593230e5df70ed9f647e953a884bec", [:mix], [{:ex_cldr, "~> 2.37", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:gettext, "~> 0.19", [hex: :gettext, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "4f7b4a5fe061734cef7b62ff29118ed6ac72698cdd7bcfc97495db73611fe0fe"},
"ex_doc": {:hex, :ex_doc, "0.31.0", "06eb1dfd787445d9cab9a45088405593dd3bb7fe99e097eaa71f37ba80c7a676", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.1", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "5350cafa6b7f77bdd107aa2199fe277acf29d739aba5aee7e865fc680c62a110"}, "ex_doc": {:hex, :ex_doc, "0.31.1", "8a2355ac42b1cc7b2379da9e40243f2670143721dd50748bf6c3b1184dae2089", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.1", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "3178c3a407c557d8343479e1ff117a96fd31bafe52a039079593fb0524ef61b0"},
"ex_ical": {:hex, :ex_ical, "0.2.0", "4b928b554614704016cc0c9ee226eb854da9327a1cc460457621ceacb1ac29a6", [:mix], [{:timex, "~> 3.1", [hex: :timex, repo: "hexpm", optional: false]}], "hexpm", "db76473b2ae0259e6633c6c479a5a4d8603f09497f55c88f9ef4d53d2b75befb"}, "ex_ical": {:hex, :ex_ical, "0.2.0", "4b928b554614704016cc0c9ee226eb854da9327a1cc460457621ceacb1ac29a6", [:mix], [{:timex, "~> 3.1", [hex: :timex, repo: "hexpm", optional: false]}], "hexpm", "db76473b2ae0259e6633c6c479a5a4d8603f09497f55c88f9ef4d53d2b75befb"},
"ex_machina": {:hex, :ex_machina, "2.7.0", "b792cc3127fd0680fecdb6299235b4727a4944a09ff0fa904cc639272cd92dc7", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "419aa7a39bde11894c87a615c4ecaa52d8f107bbdd81d810465186f783245bf8"}, "ex_machina": {:hex, :ex_machina, "2.7.0", "b792cc3127fd0680fecdb6299235b4727a4944a09ff0fa904cc639272cd92dc7", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "419aa7a39bde11894c87a615c4ecaa52d8f107bbdd81d810465186f783245bf8"},
"ex_optimizer": {:hex, :ex_optimizer, "0.1.1", "62da37e206fc2233ff7a4e54e40eae365c40f96c81992fcd15b782eb25169b80", [:mix], [{:file_info, "~> 0.0.4", [hex: :file_info, repo: "hexpm", optional: false]}], "hexpm", "e6f5c059bcd58b66be2f6f257fdc4f69b74b0fa5c9ddd669486af012e4b52286"}, "ex_optimizer": {:hex, :ex_optimizer, "0.1.1", "62da37e206fc2233ff7a4e54e40eae365c40f96c81992fcd15b782eb25169b80", [:mix], [{:file_info, "~> 0.0.4", [hex: :file_info, repo: "hexpm", optional: false]}], "hexpm", "e6f5c059bcd58b66be2f6f257fdc4f69b74b0fa5c9ddd669486af012e4b52286"},
@ -55,11 +55,11 @@
"exkismet": {:git, "https://github.com/tcitworld/exkismet.git", "8b5485fde00fafbde20f315bec387a77f7358334", []}, "exkismet": {:git, "https://github.com/tcitworld/exkismet.git", "8b5485fde00fafbde20f315bec387a77f7358334", []},
"expo": {:hex, :expo, "0.5.1", "249e826a897cac48f591deba863b26c16682b43711dd15ee86b92f25eafd96d9", [:mix], [], "hexpm", "68a4233b0658a3d12ee00d27d37d856b1ba48607e7ce20fd376958d0ba6ce92b"}, "expo": {:hex, :expo, "0.5.1", "249e826a897cac48f591deba863b26c16682b43711dd15ee86b92f25eafd96d9", [:mix], [], "hexpm", "68a4233b0658a3d12ee00d27d37d856b1ba48607e7ce20fd376958d0ba6ce92b"},
"export": {:hex, :export, "0.1.1", "6dfd268b0692428f89b9285859a2dc02b6dcd2e8fdfbca34ac6e6a331351df91", [:mix], [{:erlport, "~> 0.9", [hex: :erlport, repo: "hexpm", optional: false]}], "hexpm", "3da7444ff4053f1824352f4bdb13fbd2c28c93c2011786fb686b649fdca1021f"}, "export": {:hex, :export, "0.1.1", "6dfd268b0692428f89b9285859a2dc02b6dcd2e8fdfbca34ac6e6a331351df91", [:mix], [{:erlport, "~> 0.9", [hex: :erlport, repo: "hexpm", optional: false]}], "hexpm", "3da7444ff4053f1824352f4bdb13fbd2c28c93c2011786fb686b649fdca1021f"},
"fast_html": {:hex, :fast_html, "2.2.0", "6c5ef1be087a4ed613b0379c13f815c4d11742b36b67bb52cee7859847c84520", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}], "hexpm", "064c4f23b4a6168f9187dac8984b056f2c531bb0787f559fd6a8b34b38aefbae"}, "fast_html": {:hex, :fast_html, "2.3.0", "08c1d8ead840dd3060ba02c761bed9f37f456a1ddfe30bcdcfee8f651cec06a6", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}], "hexpm", "f18e3c7668f82d3ae0b15f48d48feeb257e28aa5ab1b0dbf781c7312e5da029d"},
"fast_sanitize": {:hex, :fast_sanitize, "0.2.3", "67b93dfb34e302bef49fec3aaab74951e0f0602fd9fa99085987af05bd91c7a5", [:mix], [{:fast_html, "~> 2.0", [hex: :fast_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.8", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "e8ad286d10d0386e15d67d0ee125245ebcfbc7d7290b08712ba9013c8c5e56e2"}, "fast_sanitize": {:hex, :fast_sanitize, "0.2.3", "67b93dfb34e302bef49fec3aaab74951e0f0602fd9fa99085987af05bd91c7a5", [:mix], [{:fast_html, "~> 2.0", [hex: :fast_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.8", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "e8ad286d10d0386e15d67d0ee125245ebcfbc7d7290b08712ba9013c8c5e56e2"},
"file_info": {:hex, :file_info, "0.0.4", "2e0e77f211e833f38ead22cb29ce53761d457d80b3ffe0ffe0eb93880b0963b2", [:mix], [{:mimetype_parser, "~> 0.1.2", [hex: :mimetype_parser, repo: "hexpm", optional: false]}], "hexpm", "50e7ad01c2c8b9339010675fe4dc4a113b8d6ca7eddce24d1d74fd0e762781a5"}, "file_info": {:hex, :file_info, "0.0.4", "2e0e77f211e833f38ead22cb29ce53761d457d80b3ffe0ffe0eb93880b0963b2", [:mix], [{:mimetype_parser, "~> 0.1.2", [hex: :mimetype_parser, repo: "hexpm", optional: false]}], "hexpm", "50e7ad01c2c8b9339010675fe4dc4a113b8d6ca7eddce24d1d74fd0e762781a5"},
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
"floki": {:hex, :floki, "0.35.2", "87f8c75ed8654b9635b311774308b2760b47e9a579dabf2e4d5f1e1d42c39e0b", [:mix], [], "hexpm", "6b05289a8e9eac475f644f09c2e4ba7e19201fd002b89c28c1293e7bd16773d9"}, "floki": {:hex, :floki, "0.35.3", "0c8c6234aa71cb2b069cf801e8f8f30f8d096eb452c3dae2ccc409510ec32720", [:mix], [], "hexpm", "6d9f07f3fc76599f3b66c39f4a81ac62c8f4d9631140268db92aacad5d0e56d4"},
"gen_smtp": {:hex, :gen_smtp, "1.2.0", "9cfc75c72a8821588b9b9fe947ae5ab2aed95a052b81237e0928633a13276fd3", [:rebar3], [{:ranch, ">= 1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "5ee0375680bca8f20c4d85f58c2894441443a743355430ff33a783fe03296779"}, "gen_smtp": {:hex, :gen_smtp, "1.2.0", "9cfc75c72a8821588b9b9fe947ae5ab2aed95a052b81237e0928633a13276fd3", [:rebar3], [{:ranch, ">= 1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "5ee0375680bca8f20c4d85f58c2894441443a743355430ff33a783fe03296779"},
"geo": {:hex, :geo, "3.6.0", "00c9c6338579f67e91cd5950af4ae2eb25cdce0c3398718c232539f61625d0bd", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "1dbdebf617183b54bc3c8ad7a36531a9a76ada8ca93f75f573b0ae94006168da"}, "geo": {:hex, :geo, "3.6.0", "00c9c6338579f67e91cd5950af4ae2eb25cdce0c3398718c232539f61625d0bd", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "1dbdebf617183b54bc3c8ad7a36531a9a76ada8ca93f75f573b0ae94006168da"},
"geo_postgis": {:hex, :geo_postgis, "3.5.0", "e3675b6276b8c2166dc20a6fa9d992eb73c665de2b09b666d09c7824dc8a8300", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:geo, "~> 3.5", [hex: :geo, repo: "hexpm", optional: false]}, {:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :poison, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: false]}], "hexpm", "0bebc5b00f8b11835066bd6213fbeeec03704b4a1c206920b81c1ec2201d185f"}, "geo_postgis": {:hex, :geo_postgis, "3.5.0", "e3675b6276b8c2166dc20a6fa9d992eb73c665de2b09b666d09c7824dc8a8300", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:geo, "~> 3.5", [hex: :geo, repo: "hexpm", optional: false]}, {:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :poison, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: false]}], "hexpm", "0bebc5b00f8b11835066bd6213fbeeec03704b4a1c206920b81c1ec2201d185f"},
@ -71,14 +71,14 @@
"guardian_db": {:hex, :guardian_db, "3.0.0", "c42902e3f1af1ba1e2d0c10913b926a1421f3a7e38eb4fc382b715c17489abdb", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.1", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:guardian, "~> 1.0 or ~> 2.0", [hex: :guardian, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "9c2ec4278efa34f9f1cc6ba795e552d41fdc7ffba5319d67eeb533b89392d183"}, "guardian_db": {:hex, :guardian_db, "3.0.0", "c42902e3f1af1ba1e2d0c10913b926a1421f3a7e38eb4fc382b715c17489abdb", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.1", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:guardian, "~> 1.0 or ~> 2.0", [hex: :guardian, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "9c2ec4278efa34f9f1cc6ba795e552d41fdc7ffba5319d67eeb533b89392d183"},
"guardian_phoenix": {:hex, :guardian_phoenix, "2.0.1", "89a817265af09a6ddf7cb1e77f17ffca90cea2db10ff888375ef34502b2731b1", [:mix], [{:guardian, "~> 2.0", [hex: :guardian, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.3", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "21f439246715192b231f228680465d1ed5fbdf01555a4a3b17165532f5f9a08c"}, "guardian_phoenix": {:hex, :guardian_phoenix, "2.0.1", "89a817265af09a6ddf7cb1e77f17ffca90cea2db10ff888375ef34502b2731b1", [:mix], [{:guardian, "~> 2.0", [hex: :guardian, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.3", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "21f439246715192b231f228680465d1ed5fbdf01555a4a3b17165532f5f9a08c"},
"hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"}, "hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"},
"hammer": {:hex, :hammer, "6.1.0", "f263e3c3e9946bd410ea0336b2abe0cb6260af4afb3a221e1027540706e76c55", [:make, :mix], [{:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}], "hexpm", "b47e415a562a6d072392deabcd58090d8a41182cf9044cdd6b0d0faaaf68ba57"}, "hammer": {:hex, :hammer, "6.2.0", "956e578f210ee67f7801caf7109b0e1145d2dad77ed5a0e5c0041a04739ede36", [:mix], [{:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}], "hexpm", "1431a30e1f9c816e0fc58d2587de2d5f4c709b74bf81be77515dc902e35bb3a7"},
"haversine": {:hex, :haversine, "0.1.0", "14240e90dae07c9459f538d12a811492f655d95fc68f999403503b4f6c4ec522", [:mix], [], "hexpm", "54dc48e895bc18a59437a37026c873634e17b648a64cb87bfafb96f64d607060"}, "haversine": {:hex, :haversine, "0.1.0", "14240e90dae07c9459f538d12a811492f655d95fc68f999403503b4f6c4ec522", [:mix], [], "hexpm", "54dc48e895bc18a59437a37026c873634e17b648a64cb87bfafb96f64d607060"},
"html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"}, "html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"},
"http_signatures": {:hex, :http_signatures, "0.1.2", "ed1cc7043abcf5bb4f30d68fb7bad9d618ec1a45c4ff6c023664e78b67d9c406", [:mix], [], "hexpm", "f08aa9ac121829dae109d608d83c84b940ef2f183ae50f2dd1e9a8bc619d8be7"}, "http_signatures": {:hex, :http_signatures, "0.1.2", "ed1cc7043abcf5bb4f30d68fb7bad9d618ec1a45c4ff6c023664e78b67d9c406", [:mix], [], "hexpm", "f08aa9ac121829dae109d608d83c84b940ef2f183ae50f2dd1e9a8bc619d8be7"},
"httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"}, "httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"},
"icalendar": {:git, "https://github.com/tcitworld/icalendar.git", "1033d922c82a7223db0ec138e2316557b70ff49f", []}, "icalendar": {:git, "https://github.com/tcitworld/icalendar.git", "1033d922c82a7223db0ec138e2316557b70ff49f", []},
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
"inet_cidr": {:hex, :inet_cidr, "1.0.4", "a05744ab7c221ca8e395c926c3919a821eb512e8f36547c062f62c4ca0cf3d6e", [:mix], [], "hexpm", "64a2d30189704ae41ca7dbdd587f5291db5d1dda1414e0774c29ffc81088c1bc"}, "inet_cidr": {:hex, :inet_cidr, "1.0.8", "d26bb7bdbdf21ae401ead2092bf2bb4bf57fe44a62f5eaa5025280720ace8a40", [:mix], [], "hexpm", "d5b26da66603bb56c933c65214c72152f0de9a6ea53618b56d63302a68f6a90e"},
"ip_reserved": {:hex, :ip_reserved, "0.1.1", "e5112d71f1abf05207f82fd9597d369a5fde1e0b6d1bbe77c02a99bb26ecdc33", [:mix], [{:inet_cidr, "~> 1.0.0", [hex: :inet_cidr, repo: "hexpm", optional: false]}], "hexpm", "55fcd2b6e211caef09ea3f54ef37d43030bec486325d12fe865ab5ed8140a4fe"}, "ip_reserved": {:hex, :ip_reserved, "0.1.1", "e5112d71f1abf05207f82fd9597d369a5fde1e0b6d1bbe77c02a99bb26ecdc33", [:mix], [{:inet_cidr, "~> 1.0.0", [hex: :inet_cidr, repo: "hexpm", optional: false]}], "hexpm", "55fcd2b6e211caef09ea3f54ef37d43030bec486325d12fe865ab5ed8140a4fe"},
"jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"},
"jose": {:hex, :jose, "1.11.6", "613fda82552128aa6fb804682e3a616f4bc15565a048dabd05b1ebd5827ed965", [:mix, :rebar3], [], "hexpm", "6275cb75504f9c1e60eeacb771adfeee4905a9e182103aa59b53fed651ff9738"}, "jose": {:hex, :jose, "1.11.6", "613fda82552128aa6fb804682e3a616f4bc15565a048dabd05b1ebd5827ed965", [:mix, :rebar3], [], "hexpm", "6275cb75504f9c1e60eeacb771adfeee4905a9e182103aa59b53fed651ff9738"},
@ -87,7 +87,7 @@
"linkify": {:hex, :linkify, "0.5.3", "5f8143d8f61f5ff08d3aeeff47ef6509492b4948d8f08007fbf66e4d2246a7f2", [:mix], [], "hexpm", "3ef35a1377d47c25506e07c1c005ea9d38d700699d92ee92825f024434258177"}, "linkify": {:hex, :linkify, "0.5.3", "5f8143d8f61f5ff08d3aeeff47ef6509492b4948d8f08007fbf66e4d2246a7f2", [:mix], [], "hexpm", "3ef35a1377d47c25506e07c1c005ea9d38d700699d92ee92825f024434258177"},
"makeup": {:hex, :makeup, "1.1.1", "fa0bc768698053b2b3869fa8a62616501ff9d11a562f3ce39580d60860c3a55e", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5dc62fbdd0de44de194898b6710692490be74baa02d9d108bc29f007783b0b48"}, "makeup": {:hex, :makeup, "1.1.1", "fa0bc768698053b2b3869fa8a62616501ff9d11a562f3ce39580d60860c3a55e", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5dc62fbdd0de44de194898b6710692490be74baa02d9d108bc29f007783b0b48"},
"makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"}, "makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"},
"makeup_erlang": {:hex, :makeup_erlang, "0.1.3", "d684f4bac8690e70b06eb52dad65d26de2eefa44cd19d64a8095e1417df7c8fd", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "b78dc853d2e670ff6390b605d807263bf606da3c82be37f9d7f68635bd886fc9"}, "makeup_erlang": {:hex, :makeup_erlang, "0.1.4", "29563475afa9b8a2add1b7a9c8fb68d06ca7737648f28398e04461f008b69521", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f4ed47ecda66de70dd817698a703f8816daa91272e7e45812469498614ae8b29"},
"meck": {:hex, :meck, "0.9.2", "85ccbab053f1db86c7ca240e9fc718170ee5bda03810a6292b5306bf31bae5f5", [:rebar3], [], "hexpm", "81344f561357dc40a8344afa53767c32669153355b626ea9fcbc8da6b3045826"}, "meck": {:hex, :meck, "0.9.2", "85ccbab053f1db86c7ca240e9fc718170ee5bda03810a6292b5306bf31bae5f5", [:rebar3], [], "hexpm", "81344f561357dc40a8344afa53767c32669153355b626ea9fcbc8da6b3045826"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
"mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"},
@ -103,20 +103,20 @@
"nimble_pool": {:hex, :nimble_pool, "0.2.6", "91f2f4c357da4c4a0a548286c84a3a28004f68f05609b4534526871a22053cde", [:mix], [], "hexpm", "1c715055095d3f2705c4e236c18b618420a35490da94149ff8b580a2144f653f"}, "nimble_pool": {:hex, :nimble_pool, "0.2.6", "91f2f4c357da4c4a0a548286c84a3a28004f68f05609b4534526871a22053cde", [:mix], [], "hexpm", "1c715055095d3f2705c4e236c18b618420a35490da94149ff8b580a2144f653f"},
"oauth2": {:hex, :oauth2, "2.1.0", "beb657f393814a3a7a8a15bd5e5776ecae341fd344df425342a3b6f1904c2989", [:mix], [{:tesla, "~> 1.5", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm", "8ac07f85b3307dd1acfeb0ec852f64161b22f57d0ce0c15e616a1dfc8ebe2b41"}, "oauth2": {:hex, :oauth2, "2.1.0", "beb657f393814a3a7a8a15bd5e5776ecae341fd344df425342a3b6f1904c2989", [:mix], [{:tesla, "~> 1.5", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm", "8ac07f85b3307dd1acfeb0ec852f64161b22f57d0ce0c15e616a1dfc8ebe2b41"},
"oauther": {:hex, :oauther, "1.3.0", "82b399607f0ca9d01c640438b34d74ebd9e4acd716508f868e864537ecdb1f76", [:mix], [], "hexpm", "78eb888ea875c72ca27b0864a6f550bc6ee84f2eeca37b093d3d833fbcaec04e"}, "oauther": {:hex, :oauther, "1.3.0", "82b399607f0ca9d01c640438b34d74ebd9e4acd716508f868e864537ecdb1f76", [:mix], [], "hexpm", "78eb888ea875c72ca27b0864a6f550bc6ee84f2eeca37b093d3d833fbcaec04e"},
"oban": {:hex, :oban, "2.17.1", "42d6221a1c17b63d81c19e3bad9ea82b59e39c47c1f9b7670ee33628569a449b", [:mix], [{:ecto_sql, "~> 3.6", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c02686ada7979b00e259c0efbafeae2749f8209747b3460001fe695c5bdbeee6"}, "oban": {:hex, :oban, "2.17.3", "ddfd5710aadcd550d2e174c8d73ce5f1865601418cf54a91775f20443fb832b7", [:mix], [{:ecto_sql, "~> 3.6", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "452eada8bfe0d0fefd0740ab5fa8cf3ef6c375df0b4a3c3805d179022a04738a"},
"paasaa": {:hex, :paasaa, "0.6.0", "07c8ed81010caa25db351d474f0c053072c809821c60f9646f7b1547bec52f6d", [:mix], [], "hexpm", "732ddfc21bac0831edb26aec468af3ec2b8997d74f6209810b1cc53199c29f2e"}, "paasaa": {:hex, :paasaa, "0.6.0", "07c8ed81010caa25db351d474f0c053072c809821c60f9646f7b1547bec52f6d", [:mix], [], "hexpm", "732ddfc21bac0831edb26aec468af3ec2b8997d74f6209810b1cc53199c29f2e"},
"parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
"phoenix": {:hex, :phoenix, "1.7.10", "02189140a61b2ce85bb633a9b6fd02dff705a5f1596869547aeb2b2b95edd729", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "cf784932e010fd736d656d7fead6a584a4498efefe5b8227e9f383bf15bb79d0"}, "phoenix": {:hex, :phoenix, "1.7.10", "02189140a61b2ce85bb633a9b6fd02dff705a5f1596869547aeb2b2b95edd729", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "cf784932e010fd736d656d7fead6a584a4498efefe5b8227e9f383bf15bb79d0"},
"phoenix_ecto": {:hex, :phoenix_ecto, "4.4.3", "86e9878f833829c3f66da03d75254c155d91d72a201eb56ae83482328dc7ca93", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "d36c401206f3011fefd63d04e8ef626ec8791975d9d107f9a0817d426f61ac07"}, "phoenix_ecto": {:hex, :phoenix_ecto, "4.4.3", "86e9878f833829c3f66da03d75254c155d91d72a201eb56ae83482328dc7ca93", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "d36c401206f3011fefd63d04e8ef626ec8791975d9d107f9a0817d426f61ac07"},
"phoenix_html": {:hex, :phoenix_html, "3.3.3", "380b8fb45912b5638d2f1d925a3771b4516b9a78587249cabe394e0a5d579dc9", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "923ebe6fec6e2e3b3e569dfbdc6560de932cd54b000ada0208b5f45024bdd76c"}, "phoenix_html": {:hex, :phoenix_html, "3.3.3", "380b8fb45912b5638d2f1d925a3771b4516b9a78587249cabe394e0a5d579dc9", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "923ebe6fec6e2e3b3e569dfbdc6560de932cd54b000ada0208b5f45024bdd76c"},
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.4.1", "2aff698f5e47369decde4357ba91fc9c37c6487a512b41732818f2204a8ef1d3", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "9bffb834e7ddf08467fe54ae58b5785507aaba6255568ae22b4d46e2bb3615ab"}, "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.4.1", "2aff698f5e47369decde4357ba91fc9c37c6487a512b41732818f2204a8ef1d3", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "9bffb834e7ddf08467fe54ae58b5785507aaba6255568ae22b4d46e2bb3615ab"},
"phoenix_live_view": {:hex, :phoenix_live_view, "0.20.3", "8b6406bc0a451f295407d7acff7f234a6314be5bbe0b3f90ed82b07f50049878", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a8e4385e05618b424779f894ed2df97d3c7518b7285fcd11979077ae6226466b"}, "phoenix_live_view": {:hex, :phoenix_live_view, "0.20.4", "0dc21e89dbf5b1f3a69090a92d1a2724bfa951d5cbccff6c5b318e12eac107e3", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d8930c9c79dd25775646874abdf3d8d24356b88d58fa14f637c8e3418d36bce3"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"},
"phoenix_swoosh": {:hex, :phoenix_swoosh, "1.2.0", "a544d83fde4a767efb78f45404a74c9e37b2a9c5ea3339692e65a6966731f935", [:mix], [{:finch, "~> 0.8", [hex: :finch, repo: "hexpm", optional: true]}, {:hackney, "~> 1.10", [hex: :hackney, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_view, "~> 1.0 or ~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:swoosh, "~> 1.5", [hex: :swoosh, repo: "hexpm", optional: false]}], "hexpm", "e88d117251e89a16b92222415a6d87b99a96747ddf674fc5c7631de734811dba"}, "phoenix_swoosh": {:hex, :phoenix_swoosh, "1.2.1", "b74ccaa8046fbc388a62134360ee7d9742d5a8ae74063f34eb050279de7a99e1", [:mix], [{:finch, "~> 0.8", [hex: :finch, repo: "hexpm", optional: true]}, {:hackney, "~> 1.10", [hex: :hackney, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_view, "~> 1.0 or ~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:swoosh, "~> 1.5", [hex: :swoosh, repo: "hexpm", optional: false]}], "hexpm", "4000eeba3f9d7d1a6bf56d2bd56733d5cadf41a7f0d8ffe5bb67e7d667e204a2"},
"phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"},
"phoenix_view": {:hex, :phoenix_view, "2.0.3", "4d32c4817fce933693741deeb99ef1392619f942633dde834a5163124813aad3", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "cd34049af41be2c627df99cd4eaa71fc52a328c0c3d8e7d4aa28f880c30e7f64"}, "phoenix_view": {:hex, :phoenix_view, "2.0.3", "4d32c4817fce933693741deeb99ef1392619f942633dde834a5163124813aad3", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "cd34049af41be2c627df99cd4eaa71fc52a328c0c3d8e7d4aa28f880c30e7f64"},
"plug": {:hex, :plug, "1.15.2", "94cf1fa375526f30ff8770837cb804798e0045fd97185f0bb9e5fcd858c792a3", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "02731fa0c2dcb03d8d21a1d941bdbbe99c2946c0db098eee31008e04c6283615"}, "plug": {:hex, :plug, "1.15.3", "712976f504418f6dff0a3e554c40d705a9bcf89a7ccef92fc6a5ef8f16a30a97", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cc4365a3c010a56af402e0809208873d113e9c38c401cabd88027ef4f5c01fd2"},
"plug_cowboy": {:hex, :plug_cowboy, "2.6.1", "9a3bbfceeb65eff5f39dab529e5cd79137ac36e913c02067dba3963a26efe9b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "de36e1a21f451a18b790f37765db198075c25875c64834bcc82d90b309eb6613"}, "plug_cowboy": {:hex, :plug_cowboy, "2.6.2", "753611b23b29231fb916b0cdd96028084b12aff57bfd7b71781bd04b1dbeb5c9", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "951ed2433df22f4c97b85fdb145d4cee561f36b74854d64c06d896d7cd2921a7"},
"plug_crypto": {:hex, :plug_crypto, "2.0.0", "77515cc10af06645abbfb5e6ad7a3e9714f805ae118fa1a70205f80d2d70fe73", [:mix], [], "hexpm", "53695bae57cc4e54566d993eb01074e4d894b65a3766f1c43e2c61a1b0f45ea9"}, "plug_crypto": {:hex, :plug_crypto, "2.0.0", "77515cc10af06645abbfb5e6ad7a3e9714f805ae118fa1a70205f80d2d70fe73", [:mix], [], "hexpm", "53695bae57cc4e54566d993eb01074e4d894b65a3766f1c43e2c61a1b0f45ea9"},
"poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"}, "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"},
"postgrex": {:hex, :postgrex, "0.17.4", "5777781f80f53b7c431a001c8dad83ee167bcebcf3a793e3906efff680ab62b3", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "6458f7d5b70652bc81c3ea759f91736c16a31be000f306d3c64bcdfe9a18b3cc"}, "postgrex": {:hex, :postgrex, "0.17.4", "5777781f80f53b7c431a001c8dad83ee167bcebcf3a793e3906efff680ab62b3", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "6458f7d5b70652bc81c3ea759f91736c16a31be000f306d3c64bcdfe9a18b3cc"},
@ -127,7 +127,7 @@
"replug": {:hex, :replug, "0.1.0", "61d35f8c873c0078a23c49579a48f36e45789414b1ec0daee3fd5f4e34221f23", [:mix], [{:plug, "~> 1.8", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "f71f7a57e944e854fe4946060c6964098e53958074c69fb844b96e0bd58cfa60"}, "replug": {:hex, :replug, "0.1.0", "61d35f8c873c0078a23c49579a48f36e45789414b1ec0daee3fd5f4e34221f23", [:mix], [{:plug, "~> 1.8", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "f71f7a57e944e854fe4946060c6964098e53958074c69fb844b96e0bd58cfa60"},
"sentry": {:hex, :sentry, "8.1.0", "8d235b62fce5f8e067ea1644e30939405b71a5e1599d9529ff82899d11d03f2b", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.6", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, "~> 2.3", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm", "f9fc7641ef61e885510f5e5963c2948b9de1de597c63f781e9d3d6c9c8681ab4"}, "sentry": {:hex, :sentry, "8.1.0", "8d235b62fce5f8e067ea1644e30939405b71a5e1599d9529ff82899d11d03f2b", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.6", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, "~> 2.3", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm", "f9fc7641ef61e885510f5e5963c2948b9de1de597c63f781e9d3d6c9c8681ab4"},
"shortuuid": {:hex, :shortuuid, "3.0.0", "028684d9eeed0ad4b800e8481afd854e1a61c526f35952455b2ee4248601e7b8", [:mix], [], "hexpm", "dfd8f80f514cbb91622cb83f4ac0d6e2f06d98cc6d4aeba94444a212289d0d39"}, "shortuuid": {:hex, :shortuuid, "3.0.0", "028684d9eeed0ad4b800e8481afd854e1a61c526f35952455b2ee4248601e7b8", [:mix], [], "hexpm", "dfd8f80f514cbb91622cb83f4ac0d6e2f06d98cc6d4aeba94444a212289d0d39"},
"sitemapper": {:hex, :sitemapper, "0.7.0", "4aee7930327a9a01b1c9b81d1d42f60c1a295e9f420108eb2d130c317415abd7", [:mix], [{:ex_aws_s3, "~> 2.0", [hex: :ex_aws_s3, repo: "hexpm", optional: true]}, {:xml_builder, "~> 2.1", [hex: :xml_builder, repo: "hexpm", optional: false]}], "hexpm", "60f7a684e5e9fe7f10ac5b69f48b0be2bcbba995afafcb3c143fc0c8ef1f223f"}, "sitemapper": {:hex, :sitemapper, "0.8.0", "50c8c85ed38c013829ce700e8a8d195a2faf4aed8685659b14529dcb6f91fee0", [:mix], [{:ex_aws_s3, "~> 2.0", [hex: :ex_aws_s3, repo: "hexpm", optional: true]}, {:xml_builder, "~> 2.1", [hex: :xml_builder, repo: "hexpm", optional: false]}], "hexpm", "7cd42b454035da457151c9b6a314b688b5bbe5383add95badc65d013c25989c5"},
"sleeplocks": {:hex, :sleeplocks, "1.1.2", "d45aa1c5513da48c888715e3381211c859af34bee9b8290490e10c90bb6ff0ca", [:rebar3], [], "hexpm", "9fe5d048c5b781d6305c1a3a0f40bb3dfc06f49bf40571f3d2d0c57eaa7f59a5"}, "sleeplocks": {:hex, :sleeplocks, "1.1.2", "d45aa1c5513da48c888715e3381211c859af34bee9b8290490e10c90bb6ff0ca", [:rebar3], [], "hexpm", "9fe5d048c5b781d6305c1a3a0f40bb3dfc06f49bf40571f3d2d0c57eaa7f59a5"},
"slugger": {:hex, :slugger, "0.3.0", "efc667ab99eee19a48913ccf3d038b1fb9f165fa4fbf093be898b8099e61b6ed", [:mix], [], "hexpm", "20d0ded0e712605d1eae6c5b4889581c3460d92623a930ddda91e0e609b5afba"}, "slugger": {:hex, :slugger, "0.3.0", "efc667ab99eee19a48913ccf3d038b1fb9f165fa4fbf093be898b8099e61b6ed", [:mix], [], "hexpm", "20d0ded0e712605d1eae6c5b4889581c3460d92623a930ddda91e0e609b5afba"},
"slugify": {:hex, :slugify, "1.3.1", "0d3b8b7e5c1eeaa960e44dce94382bee34a39b3ea239293e457a9c5b47cc6fd3", [:mix], [], "hexpm", "cb090bbeb056b312da3125e681d98933a360a70d327820e4b7f91645c4d8be76"}, "slugify": {:hex, :slugify, "1.3.1", "0d3b8b7e5c1eeaa960e44dce94382bee34a39b3ea239293e457a9c5b47cc6fd3", [:mix], [], "hexpm", "cb090bbeb056b312da3125e681d98933a360a70d327820e4b7f91645c4d8be76"},
@ -135,14 +135,14 @@
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
"struct_access": {:hex, :struct_access, "1.1.2", "a42e6ceedd9b9ea090ee94a6da089d56e16f374dbbc010c3eebdf8be17df286f", [:mix], [], "hexpm", "e4c411dcc0226081b95709909551fc92b8feb1a3476108348ea7e3f6c12e586a"}, "struct_access": {:hex, :struct_access, "1.1.2", "a42e6ceedd9b9ea090ee94a6da089d56e16f374dbbc010c3eebdf8be17df286f", [:mix], [], "hexpm", "e4c411dcc0226081b95709909551fc92b8feb1a3476108348ea7e3f6c12e586a"},
"sweet_xml": {:hex, :sweet_xml, "0.7.4", "a8b7e1ce7ecd775c7e8a65d501bc2cd933bff3a9c41ab763f5105688ef485d08", [:mix], [], "hexpm", "e7c4b0bdbf460c928234951def54fe87edf1a170f6896675443279e2dbeba167"}, "sweet_xml": {:hex, :sweet_xml, "0.7.4", "a8b7e1ce7ecd775c7e8a65d501bc2cd933bff3a9c41ab763f5105688ef485d08", [:mix], [], "hexpm", "e7c4b0bdbf460c928234951def54fe87edf1a170f6896675443279e2dbeba167"},
"swoosh": {:hex, :swoosh, "1.14.3", "949e6bf6dd469449238a94ec6f19ec10b63fc8753de7f3ebe3d3aeaf772f4c6b", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.4 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6c565103fc8f086bdd96e5c948660af8e20922b7a90a75db261f06a34f805c8b"}, "swoosh": {:hex, :swoosh, "1.15.2", "490ea85a98e8fb5178c07039e0d8519839e38127724a58947a668c00db7574ee", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.4 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9f7739c02f6c7c0ca82ee397f3bfe0465dbe4c8a65372ac2a5584bf147dd5831"},
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
"tesla": {:hex, :tesla, "1.8.0", "d511a4f5c5e42538d97eef7c40ec4f3e44effdc5068206f42ed859e09e51d1fd", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "10501f360cd926a309501287470372af1a6e1cbed0f43949203a4c13300bc79f"}, "tesla": {:hex, :tesla, "1.8.0", "d511a4f5c5e42538d97eef7c40ec4f3e44effdc5068206f42ed859e09e51d1fd", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "10501f360cd926a309501287470372af1a6e1cbed0f43949203a4c13300bc79f"},
"timex": {:hex, :timex, "3.7.11", "bb95cb4eb1d06e27346325de506bcc6c30f9c6dea40d1ebe390b262fad1862d1", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.20", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "8b9024f7efbabaf9bd7aa04f65cf8dcd7c9818ca5737677c7b76acbc6a94d1aa"}, "timex": {:hex, :timex, "3.7.11", "bb95cb4eb1d06e27346325de506bcc6c30f9c6dea40d1ebe390b262fad1862d1", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.20", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "8b9024f7efbabaf9bd7aa04f65cf8dcd7c9818ca5737677c7b76acbc6a94d1aa"},
"tls_certificate_check": {:hex, :tls_certificate_check, "1.21.0", "042ab2c0c860652bc5cf69c94e3a31f96676d14682e22ec7813bd173ceff1788", [:rebar3], [{:ssl_verify_fun, "~> 1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "6cee6cffc35a390840d48d463541d50746a7b0e421acaadb833cfc7961e490e7"}, "tls_certificate_check": {:hex, :tls_certificate_check, "1.21.0", "042ab2c0c860652bc5cf69c94e3a31f96676d14682e22ec7813bd173ceff1788", [:rebar3], [{:ssl_verify_fun, "~> 1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "6cee6cffc35a390840d48d463541d50746a7b0e421acaadb833cfc7961e490e7"},
"tz_world": {:hex, :tz_world, "1.3.2", "15d331ad1ff22735dfcc8c98bfc7b2a9fdc17f1f071e31e21cdafe2d9318a300", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.5", [hex: :certifi, repo: "hexpm", optional: true]}, {:geo, "~> 1.0 or ~> 2.0 or ~> 3.3", [hex: :geo, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "d1a345e07b3378c4c902ad54fbd5d54c8c3dd55dba883b7407fe57bcec45ff2a"}, "tz_world": {:hex, :tz_world, "1.3.2", "15d331ad1ff22735dfcc8c98bfc7b2a9fdc17f1f071e31e21cdafe2d9318a300", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.5", [hex: :certifi, repo: "hexpm", optional: true]}, {:geo, "~> 1.0 or ~> 2.0 or ~> 3.3", [hex: :geo, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "d1a345e07b3378c4c902ad54fbd5d54c8c3dd55dba883b7407fe57bcec45ff2a"},
"tzdata": {:hex, :tzdata, "1.1.1", "20c8043476dfda8504952d00adac41c6eda23912278add38edc140ae0c5bcc46", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a69cec8352eafcd2e198dea28a34113b60fdc6cb57eb5ad65c10292a6ba89787"}, "tzdata": {:hex, :tzdata, "1.1.1", "20c8043476dfda8504952d00adac41c6eda23912278add38edc140ae0c5bcc46", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a69cec8352eafcd2e198dea28a34113b60fdc6cb57eb5ad65c10292a6ba89787"},
"ueberauth": {:hex, :ueberauth, "0.10.5", "806adb703df87e55b5615cf365e809f84c20c68aa8c08ff8a416a5a6644c4b02", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "3efd1f31d490a125c7ed453b926f7c31d78b97b8a854c755f5c40064bf3ac9e1"}, "ueberauth": {:hex, :ueberauth, "0.10.7", "5a31cbe11e7ce5c7484d745dc9e1f11948e89662f8510d03c616de03df581ebd", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "0bccf73e2ffd6337971340832947ba232877aa8122dba4c95be9f729c8987377"},
"ueberauth_cas": {:hex, :ueberauth_cas, "2.3.1", "df45a1f2c5df8bc80191cbca4baeeed808d697702ec5ebe5bd5d5a264481752f", [:mix], [{:httpoison, "~> 1.8", [hex: :httpoison, repo: "hexpm", optional: false]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.6", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm", "5068ae2b9e217c2f05aa9a67483a6531e21ba0be9a6f6c8749bb7fd1599be321"}, "ueberauth_cas": {:hex, :ueberauth_cas, "2.3.1", "df45a1f2c5df8bc80191cbca4baeeed808d697702ec5ebe5bd5d5a264481752f", [:mix], [{:httpoison, "~> 1.8", [hex: :httpoison, repo: "hexpm", optional: false]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.6", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm", "5068ae2b9e217c2f05aa9a67483a6531e21ba0be9a6f6c8749bb7fd1599be321"},
"ueberauth_discord": {:hex, :ueberauth_discord, "0.7.0", "463f6dfe1ed10a76739331ce8e1dd3600ab611f10524dd828eb3aa50e76e9d43", [:mix], [{:oauth2, "~> 1.0 or ~> 2.0", [hex: :oauth2, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.7", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm", "d6f98ef91abb4ddceada4b7acba470e0e68c4d2de9735ff2f24172a8e19896b4"}, "ueberauth_discord": {:hex, :ueberauth_discord, "0.7.0", "463f6dfe1ed10a76739331ce8e1dd3600ab611f10524dd828eb3aa50e76e9d43", [:mix], [{:oauth2, "~> 1.0 or ~> 2.0", [hex: :oauth2, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.7", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm", "d6f98ef91abb4ddceada4b7acba470e0e68c4d2de9735ff2f24172a8e19896b4"},
"ueberauth_facebook": {:hex, :ueberauth_facebook, "0.10.0", "0d607fbd1b7c6e0449981571027d869c2d156b8ad20c42e3672346678c05ccf1", [:mix], [{:oauth2, "~> 1.0 or ~> 2.0", [hex: :oauth2, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.7", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm", "bf8ce5d66b1c50da8abff77e8086c1b710bdde63f4acaef19a651ba43a9537a8"}, "ueberauth_facebook": {:hex, :ueberauth_facebook, "0.10.0", "0d607fbd1b7c6e0449981571027d869c2d156b8ad20c42e3672346678c05ccf1", [:mix], [{:oauth2, "~> 1.0 or ~> 2.0", [hex: :oauth2, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.7", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm", "bf8ce5d66b1c50da8abff77e8086c1b710bdde63f4acaef19a651ba43a9537a8"},

2823
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -15,8 +15,9 @@
"story:preview": "histoire preview", "story:preview": "histoire preview",
"test": "vitest", "test": "vitest",
"coverage": "vitest run --coverage", "coverage": "vitest run --coverage",
"prepare": "husky install", "prepare": "husky",
"pre-commit": "lint-staged" "pre-commit": "lint-staged",
"postinstall": "patch-package"
}, },
"lint-staged": { "lint-staged": {
"**/*.{js,ts,vue}": [ "**/*.{js,ts,vue}": [
@ -28,6 +29,7 @@
"mix credo" "mix credo"
] ]
}, },
"type": "module",
"dependencies": { "dependencies": {
"@apollo/client": "^3.3.16", "@apollo/client": "^3.3.16",
"@framasoft/socket": "^1.0.0", "@framasoft/socket": "^1.0.0",
@ -73,7 +75,7 @@
"blurhash": "^2.0.0", "blurhash": "^2.0.0",
"date-fns": "^2.16.0", "date-fns": "^2.16.0",
"date-fns-tz": "^2.0.0", "date-fns-tz": "^2.0.0",
"floating-vue": "^2.0.0-beta.24", "floating-vue": "^5.0.0",
"graphql": "^16.8.1", "graphql": "^16.8.1",
"graphql-tag": "^2.10.3", "graphql-tag": "^2.10.3",
"hammerjs": "^2.0.8", "hammerjs": "^2.0.8",
@ -85,6 +87,7 @@
"lodash": "^4.17.11", "lodash": "^4.17.11",
"ngeohash": "^0.6.3", "ngeohash": "^0.6.3",
"p-debounce": "^4.0.0", "p-debounce": "^4.0.0",
"patch-package": "^8.0.0",
"phoenix": "^1.6", "phoenix": "^1.6",
"postcss": "^8", "postcss": "^8",
"register-service-worker": "^1.7.2", "register-service-worker": "^1.7.2",
@ -92,7 +95,7 @@
"tailwindcss": "^3", "tailwindcss": "^3",
"tippy.js": "^6.2.3", "tippy.js": "^6.2.3",
"unfetch": "^5.0.0", "unfetch": "^5.0.0",
"vue": "^3.2.37", "vue": "3.4.16",
"vue-i18n": "9", "vue-i18n": "9",
"vue-material-design-icons": "^5.1.2", "vue-material-design-icons": "^5.1.2",
"vue-matomo": "^4.1.0", "vue-matomo": "^4.1.0",
@ -116,9 +119,9 @@
"@types/ngeohash": "^0.6.2", "@types/ngeohash": "^0.6.2",
"@types/phoenix": "^1.5.2", "@types/phoenix": "^1.5.2",
"@types/sanitize-html": "^2.5.0", "@types/sanitize-html": "^2.5.0",
"@vitejs/plugin-vue": "^4.0.0", "@vitejs/plugin-vue": "^5.0.0",
"@vitest/coverage-v8": "^0.34.1", "@vitest/coverage-v8": "^1.2.2",
"@vitest/ui": "^0.34.1", "@vitest/ui": "^1.2.2",
"@vue/eslint-config-prettier": "^9.0.0", "@vue/eslint-config-prettier": "^9.0.0",
"@vue/eslint-config-typescript": "^12.0.0", "@vue/eslint-config-typescript": "^12.0.0",
"@vue/test-utils": "^2.0.2", "@vue/test-utils": "^2.0.2",
@ -129,8 +132,8 @@
"eslint-plugin-vue": "^9.3.0", "eslint-plugin-vue": "^9.3.0",
"flush-promises": "^1.0.2", "flush-promises": "^1.0.2",
"histoire": "^0.17.0", "histoire": "^0.17.0",
"husky": "^8.0.3", "husky": "^9.0.10",
"jsdom": "^22.0.0", "jsdom": "^24.0.0",
"lint-staged": "^15.1.0", "lint-staged": "^15.1.0",
"mock-apollo-client": "^1.1.0", "mock-apollo-client": "^1.1.0",
"prettier": "^3.0.0", "prettier": "^3.0.0",
@ -138,9 +141,9 @@
"rollup-plugin-visualizer": "^5.7.1", "rollup-plugin-visualizer": "^5.7.1",
"sass": "^1.34.1", "sass": "^1.34.1",
"typescript": "~5.3.2", "typescript": "~5.3.2",
"vite": "^4.5.0", "vite": "^5.0.12",
"vite-plugin-pwa": "^0.17.0", "vite-plugin-pwa": "^0.17.0",
"vitest": "^0.34.1", "vitest": "^1.2.2",
"vue-i18n-extract": "^2.0.4", "vue-i18n-extract": "^2.0.4",
"vue-router-mock": "^1.0.0" "vue-router-mock": "^1.0.0"
} }

View File

@ -0,0 +1,66 @@
diff --git a/node_modules/vue-i18n-extract/dist/vue-i18n-extract.modern.mjs b/node_modules/vue-i18n-extract/dist/vue-i18n-extract.modern.mjs
index 670733e..872d1af 100644
--- a/node_modules/vue-i18n-extract/dist/vue-i18n-extract.modern.mjs
+++ b/node_modules/vue-i18n-extract/dist/vue-i18n-extract.modern.mjs
@@ -38,7 +38,7 @@ var defaultConfig = {
};
function initCommand() {
- fs.writeFileSync(path.resolve(process.cwd(), './vue-i18n-extract.config.js'), `module.exports = ${JSON.stringify(defaultConfig, null, 2)}`);
+ fs.writeFileSync(path.resolve(process.cwd(), './vue-i18n-extract.config.cjs'), `module.exports = ${JSON.stringify(defaultConfig, null, 2)}`);
}
function resolveConfig() {
const argvOptions = cac().parse(process.argv, {
@@ -47,7 +47,7 @@ function resolveConfig() {
let options;
try {
- const pathToConfigFile = path.resolve(process.cwd(), './vue-i18n-extract.config.js'); // eslint-disable-next-line @typescript-eslint/no-var-requires
+ const pathToConfigFile = path.resolve(process.cwd(), './vue-i18n-extract.config.cjs'); // eslint-disable-next-line @typescript-eslint/no-var-requires
const configOptions = require(pathToConfigFile);
diff --git a/node_modules/vue-i18n-extract/dist/vue-i18n-extract.umd.js b/node_modules/vue-i18n-extract/dist/vue-i18n-extract.umd.js
index ca19c7a..11cb846 100644
--- a/node_modules/vue-i18n-extract/dist/vue-i18n-extract.umd.js
+++ b/node_modules/vue-i18n-extract/dist/vue-i18n-extract.umd.js
@@ -45,7 +45,7 @@
};
function initCommand() {
- fs__default["default"].writeFileSync(path__default["default"].resolve(process.cwd(), './vue-i18n-extract.config.js'), `module.exports = ${JSON.stringify(defaultConfig, null, 2)}`);
+ fs__default["default"].writeFileSync(path__default["default"].resolve(process.cwd(), './vue-i18n-extract.config.cjs'), `module.exports = ${JSON.stringify(defaultConfig, null, 2)}`);
}
function resolveConfig() {
const argvOptions = cac__default["default"]().parse(process.argv, {
@@ -54,7 +54,7 @@
let options;
try {
- const pathToConfigFile = path__default["default"].resolve(process.cwd(), './vue-i18n-extract.config.js'); // eslint-disable-next-line @typescript-eslint/no-var-requires
+ const pathToConfigFile = path__default["default"].resolve(process.cwd(), './vue-i18n-extract.config.cjs'); // eslint-disable-next-line @typescript-eslint/no-var-requires
const configOptions = require(pathToConfigFile);
diff --git a/node_modules/vue-i18n-extract/src/config-file/index.ts b/node_modules/vue-i18n-extract/src/config-file/index.ts
index 3db836f..744bd74 100644
--- a/node_modules/vue-i18n-extract/src/config-file/index.ts
+++ b/node_modules/vue-i18n-extract/src/config-file/index.ts
@@ -5,7 +5,7 @@ import defaultConfig from './vue-i18n-extract.config';
export function initCommand(): void {
fs.writeFileSync(
- path.resolve(process.cwd(), './vue-i18n-extract.config.js'),
+ path.resolve(process.cwd(), './vue-i18n-extract.config.cjs'),
`module.exports = ${JSON.stringify(defaultConfig, null, 2)}`,
);
}
@@ -16,7 +16,7 @@ export function resolveConfig (): Record<string, string> {
let options;
try {
- const pathToConfigFile = path.resolve(process.cwd(), './vue-i18n-extract.config.js');
+ const pathToConfigFile = path.resolve(process.cwd(), './vue-i18n-extract.config.cjs');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const configOptions = require(pathToConfigFile);

View File

@ -1,4 +1,4 @@
module.exports = { export default {
plugins: { plugins: {
tailwindcss: {}, tailwindcss: {},
autoprefixer: {}, autoprefixer: {},

View File

@ -146,22 +146,37 @@ body {
.taginput-item { .taginput-item {
@apply bg-primary mr-2; @apply bg-primary mr-2;
} }
.taginput-autocomplete {
@apply flex-1 !drop-shadow-none;
}
.taginput-expanded {
@apply w-full;
}
.taginput .autocomplete .dropdown-menu {
@apply w-full;
}
.taginput-container {
@apply border-none;
}
.taginput-item:first-child { .taginput-item:first-child {
@apply ml-2; @apply ml-2;
} }
.taginput-input-wrapper {
@apply block;
}
/* Autocomplete */ /* Autocomplete */
.autocomplete { .autocomplete {
@apply max-h-[200px] drop-shadow-md text-black z-10; @apply max-h-[200px] drop-shadow-md text-black z-10;
} }
.autocomplete-item { .autocomplete .autocomplete-item {
@apply py-1.5 px-4 text-start; @apply text-start p-2;
} }
.autocomplete-item-group-title { .autocomplete .autocomplete-item-group-title {
@apply opacity-50 py-0 px-2; @apply opacity-50 py-1.5 px-2 dark:text-white dark:opacity-75;
} }
/* Dropdown */ /* Dropdown */
@ -173,7 +188,7 @@ body {
@apply bg-white dark:bg-zinc-700 shadow-lg rounded text-start py-2; @apply bg-white dark:bg-zinc-700 shadow-lg rounded text-start py-2;
} }
.dropdown-item { .dropdown-item {
@apply relative inline-flex gap-1 no-underline p-2 cursor-pointer w-full; @apply relative inline-flex gap-1 no-underline p-2 cursor-pointer w-full hover:bg-[#f5f5f5] hover:text-black;
} }
.dropdown-item-active { .dropdown-item-active {
@ -343,8 +358,8 @@ button.menubar__button {
.o-drop__menu--active { .o-drop__menu--active {
@apply z-50; @apply z-50;
} }
.o-dpck__box { .datepicker-box {
@apply px-4 py-1; @apply block px-4 py-1 hover:bg-transparent;
} }
.o-dpck__header { .o-dpck__header {
@apply pb-2 mb-2; @apply pb-2 mb-2;
@ -352,7 +367,7 @@ button.menubar__button {
} }
.o-dpck__header__next, .o-dpck__header__next,
.o-dpck__header__previous { .o-dpck__header__previous {
@apply justify-center text-center no-underline cursor-pointer items-center shadow-none inline-flex relative select-none leading-6 border rounded h-10 p-2 m-1 dark:text-white; @apply justify-center text-center no-underline cursor-pointer items-center shadow-none inline-flex relative select-none leading-6 border rounded h-10 p-2 m-1 dark:text-white hover:px-2;
min-width: 2.25em; min-width: 2.25em;
} }
.o-dpck__header__list { .o-dpck__header__list {

View File

@ -30,7 +30,9 @@
class="flex-1 min-w-[200px]" class="flex-1 min-w-[200px]"
> >
<template v-slot="props"> <template v-slot="props">
<div class="dark:bg-violet-3 p-1 flex items-center gap-1"> <div
class="dark:bg-violet-3 p-1 flex items-center gap-1 flex-1 dark:text-white"
>
<div class=""> <div class="">
<img <img
v-if=" v-if="
@ -41,6 +43,7 @@
width="24" width="24"
height="24" height="24"
alt="" alt=""
class="dark:fill-white"
/> />
<o-icon v-else-if="props.option.icon" :icon="props.option.icon" /> <o-icon v-else-if="props.option.icon" :icon="props.option.icon" />
<o-icon v-else icon="help-circle" /> <o-icon v-else icon="help-circle" />

View File

@ -39,7 +39,9 @@
<o-icon :icon="addressToPoiInfos(option).poiIcon.icon" /> <o-icon :icon="addressToPoiInfos(option).poiIcon.icon" />
<b>{{ addressToPoiInfos(option).name }}</b> <b>{{ addressToPoiInfos(option).name }}</b>
</p> </p>
<small>{{ addressToPoiInfos(option).alternativeName }}</small> <p class="text-small">
{{ addressToPoiInfos(option).alternativeName }}
</p>
</template> </template>
<template #empty> <template #empty>
<template v-if="isFetching">{{ t("Searching") }}</template> <template v-if="isFetching">{{ t("Searching") }}</template>

View File

@ -1,15 +1,17 @@
<template> <template>
<o-field :label-for="id"> <o-field :label-for="id" class="taginput-field">
<template #label> <template #label>
{{ $t("Add some tags") }} <p class="inline-flex items-center gap-0.5">
<o-tooltip {{ t("Add some tags") }}
variant="dark" <o-tooltip
:label=" variant="dark"
$t('You can add tags by hitting the Enter key or by adding a comma') :label="
" t('You can add tags by hitting the Enter key or by adding a comma')
> "
<HelpCircleOutline :size="16" /> >
</o-tooltip> <HelpCircleOutline :size="16" />
</o-tooltip>
</p>
</template> </template>
<o-taginput <o-taginput
v-model="tagsStrings" v-model="tagsStrings"
@ -20,10 +22,11 @@
icon="label" icon="label"
:maxlength="20" :maxlength="20"
:maxitems="10" :maxitems="10"
:placeholder="$t('Eg: Stockholm, Dance, Chess…')" :placeholder="t('Eg: Stockholm, Dance, Chess…')"
@input="debouncedGetFilteredTags" @input="getFilteredTags"
:id="id" :id="id"
dir="auto" dir="auto"
expanded
> >
</o-taginput> </o-taginput>
</o-field> </o-field>
@ -31,11 +34,11 @@
<script lang="ts" setup> <script lang="ts" setup>
import differenceBy from "lodash/differenceBy"; import differenceBy from "lodash/differenceBy";
import { ITag } from "../../types/tag.model"; import { ITag } from "../../types/tag.model";
import debounce from "lodash/debounce";
import { computed, onBeforeMount, ref } from "vue"; import { computed, onBeforeMount, ref } from "vue";
import HelpCircleOutline from "vue-material-design-icons/HelpCircleOutline.vue"; import HelpCircleOutline from "vue-material-design-icons/HelpCircleOutline.vue";
import { useFetchTags } from "@/composition/apollo/tags"; import { useFetchTags } from "@/composition/apollo/tags";
import { FILTER_TAGS } from "@/graphql/tags"; import { FILTER_TAGS } from "@/graphql/tags";
import { useI18n } from "vue-i18n";
const props = defineProps<{ const props = defineProps<{
modelValue: ITag[]; modelValue: ITag[];
@ -47,6 +50,8 @@ const text = ref("");
const tags = ref<ITag[]>([]); const tags = ref<ITag[]>([]);
const { t } = useI18n({ useScope: "global" });
let componentId = 0; let componentId = 0;
onBeforeMount(() => { onBeforeMount(() => {
@ -61,14 +66,16 @@ const { load: fetchTags } = useFetchTags();
const getFilteredTags = async (newText: string): Promise<void> => { const getFilteredTags = async (newText: string): Promise<void> => {
text.value = newText; text.value = newText;
const res = await fetchTags(FILTER_TAGS, { filter: newText }); const res = await fetchTags(
FILTER_TAGS,
{ filter: newText },
{ debounce: 200 }
);
if (res) { if (res) {
tags.value = res.tags; tags.value = res.tags;
} }
}; };
const debouncedGetFilteredTags = debounce(getFilteredTags, 200);
const filteredTags = computed((): ITag[] => { const filteredTags = computed((): ITag[] => {
return differenceBy(tags.value, props.modelValue, "id").filter( return differenceBy(tags.value, props.modelValue, "id").filter(
(option) => (option) =>

View File

@ -32,6 +32,7 @@
v-if="currentActor" v-if="currentActor"
:currentActor="currentActor" :currentActor="currentActor"
:placeholder="t('Write a new message')" :placeholder="t('Write a new message')"
:required="true"
/> />
<o-notification <o-notification
class="my-2" class="my-2"
@ -133,6 +134,7 @@ const sendForm = (e: Event) => {
e.preventDefault(); e.preventDefault();
console.debug("Sending new private message"); console.debug("Sending new private message");
if (!currentActor.value?.id || !event.value.id) return; if (!currentActor.value?.id || !event.value.id) return;
errors.value = [];
eventPrivateMessageMutate({ eventPrivateMessageMutate({
text: text.value, text: text.value,
actorId: actorId:
@ -150,7 +152,10 @@ onEventPrivateMessageMutated(() => {
onEventPrivateMessageError((err) => { onEventPrivateMessageError((err) => {
err.graphQLErrors.forEach((error) => { err.graphQLErrors.forEach((error) => {
errors.value.push(error.message); const message = Array.isArray(error.message)
? error.message
: [error.message];
errors.value.push(...message);
}); });
}); });

View File

@ -6,7 +6,7 @@
<section> <section>
<div <div
class="flex gap-1 flex-row mb-3 bg-mbz-yellow p-3 rounded items-center" class="flex gap-1 flex-row mb-3 bg-mbz-yellow dark:text-black p-3 rounded items-center"
> >
<o-icon <o-icon
icon="alert" icon="alert"

View File

@ -1,6 +1,6 @@
<template> <template>
<span <span
class="rounded-md truncate text-sm text-violet-title px-2 py-1" class="rounded-md truncate text-sm text-black px-2 py-1"
:class="[ :class="[
typeClasses, typeClasses,
capitalize, capitalize,

View File

@ -217,7 +217,16 @@
</button> </button>
</bubble-menu> </bubble-menu>
<editor-content class="editor__content" :editor="editor" v-if="editor" /> <editor-content
class="editor__content"
:class="{ editorErrorStatus, editorIsFocused: focused }"
:editor="editor"
v-if="editor"
ref="editorContentRef"
/>
<p v-if="editorErrorMessage" class="text-sm text-mbz-danger">
{{ editorErrorMessage }}
</p>
</div> </div>
</div> </div>
</template> </template>
@ -249,7 +258,7 @@ import Underline from "@tiptap/extension-underline";
import Link from "@tiptap/extension-link"; import Link from "@tiptap/extension-link";
import { AutoDir } from "./Editor/Autodir"; import { AutoDir } from "./Editor/Autodir";
// import sanitizeHtml from "sanitize-html"; // import sanitizeHtml from "sanitize-html";
import { computed, inject, onBeforeUnmount, watch } from "vue"; import { computed, inject, onBeforeUnmount, ref, watch } from "vue";
import { Dialog } from "@/plugins/dialog"; import { Dialog } from "@/plugins/dialog";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { useMutation } from "@vue/apollo-composable"; import { useMutation } from "@vue/apollo-composable";
@ -269,6 +278,7 @@ import FormatQuoteClose from "vue-material-design-icons/FormatQuoteClose.vue";
import Undo from "vue-material-design-icons/Undo.vue"; import Undo from "vue-material-design-icons/Undo.vue";
import Redo from "vue-material-design-icons/Redo.vue"; import Redo from "vue-material-design-icons/Redo.vue";
import Placeholder from "@tiptap/extension-placeholder"; import Placeholder from "@tiptap/extension-placeholder";
import { useFocusWithin } from "@vueuse/core";
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
@ -279,11 +289,13 @@ const props = withDefaults(
currentActor: IPerson; currentActor: IPerson;
placeholder?: string; placeholder?: string;
headingLevel?: Level[]; headingLevel?: Level[];
required?: boolean;
}>(), }>(),
{ {
mode: "description", mode: "description",
maxSize: 100_000_000, maxSize: 100_000_000,
headingLevel: () => [3, 4, 5], headingLevel: () => [3, 4, 5],
required: false,
} }
); );
@ -333,7 +345,7 @@ const editor = useEditor({
"aria-label": ariaLabel.value ?? "", "aria-label": ariaLabel.value ?? "",
role: "textbox", role: "textbox",
class: class:
"prose dark:prose-invert prose-sm lg:prose-lg xl:prose-xl bg-zinc-50 dark:bg-zinc-700 focus:outline-none !max-w-full", "prose dark:prose-invert prose-sm lg:prose-lg xl:prose-xl bg-white dark:bg-zinc-700 !max-w-full",
}, },
transformPastedHTML: transformPastedHTML, transformPastedHTML: transformPastedHTML,
}, },
@ -373,8 +385,18 @@ const editor = useEditor({
onUpdate: () => { onUpdate: () => {
emit("update:modelValue", editor.value?.getHTML()); emit("update:modelValue", editor.value?.getHTML());
}, },
onBlur: () => {
checkEditorEmpty();
},
onFocus: () => {
editorErrorStatus.value = false;
editorErrorMessage.value = "";
},
}); });
const editorContentRef = ref(null);
const { focused } = useFocusWithin(editorContentRef);
watch(value, (val: string) => { watch(value, (val: string) => {
if (!editor.value) return; if (!editor.value) return;
if (val !== editor.value.getHTML()) { if (val !== editor.value.getHTML()) {
@ -470,6 +492,18 @@ defineExpose({ replyToComment, focus });
onBeforeUnmount(() => { onBeforeUnmount(() => {
editor.value?.destroy(); editor.value?.destroy();
}); });
const editorErrorStatus = ref(false);
const editorErrorMessage = ref("");
const isEmpty = computed(
() => props.required === true && editor.value?.isEmpty === true
);
const checkEditorEmpty = () => {
editorErrorStatus.value = isEmpty.value;
editorErrorMessage.value = isEmpty.value ? t("You need to enter a text") : "";
};
</script> </script>
<style lang="scss"> <style lang="scss">
@use "@/styles/_mixins" as *; @use "@/styles/_mixins" as *;
@ -523,14 +557,8 @@ onBeforeUnmount(() => {
&__content { &__content {
div.ProseMirror { div.ProseMirror {
min-height: 2.5rem; min-height: 2.5rem;
box-shadow: inset 0 1px 2px rgba(10, 10, 10, 0.1);
border-radius: 4px; border-radius: 4px;
border: 1px solid #dbdbdb;
padding: 12px 6px; padding: 12px 6px;
&:focus {
outline: none;
}
} }
h1 { h1 {
@ -655,4 +683,19 @@ onBeforeUnmount(() => {
.mention[data-id] { .mention[data-id] {
@apply inline-block border border-zinc-600 dark:border-zinc-300 rounded py-0.5 px-1; @apply inline-block border border-zinc-600 dark:border-zinc-300 rounded py-0.5 px-1;
} }
.editor__content > div {
@apply border rounded border-[#6b7280];
}
.editorIsFocused > div {
@apply ring-2 ring-[#2563eb] outline-2 outline outline-offset-2 outline-transparent;
}
.editorErrorStatus {
@apply border-red-500;
}
.editor__content p.is-editor-empty:first-child::before {
@apply text-slate-300;
}
</style> </style>

View File

@ -17,7 +17,6 @@ export function useGroupMembers(
groupName: Ref<string>, groupName: Ref<string>,
options: useGroupMembersOptions = {} options: useGroupMembersOptions = {}
) { ) {
console.debug("useGroupMembers", options);
const { result, error, loading, onResult, onError, refetch, fetchMore } = const { result, error, loading, onResult, onError, refetch, fetchMore } =
useQuery< useQuery<
{ {

View File

@ -13,6 +13,9 @@ export const COMMENT_FIELDS_FRAGMENT = gql`
actor { actor {
...ActorFragment ...ActorFragment
} }
attributedTo {
...ActorFragment
}
totalReplies totalReplies
insertedAt insertedAt
updatedAt updatedAt

View File

@ -12,6 +12,9 @@ export const CONVERSATION_QUERY_FRAGMENT = gql`
lastComment { lastComment {
...CommentFields ...CommentFields
} }
originComment {
...CommentFields
}
participants { participants {
...ActorFragment ...ActorFragment
} }
@ -19,6 +22,12 @@ export const CONVERSATION_QUERY_FRAGMENT = gql`
id id
uuid uuid
title title
organizerActor {
id
}
attributedTo {
id
}
picture { picture {
id id
url url

View File

@ -1642,5 +1642,7 @@
"Visit {instance_domain}": "Visit {instance_domain}", "Visit {instance_domain}": "Visit {instance_domain}",
"Software details: {software_details}": "Software details: {software_details}", "Software details: {software_details}": "Software details: {software_details}",
"Only instances with an application actor can be followed": "Only instances with an application actor can be followed", "Only instances with an application actor can be followed": "Only instances with an application actor can be followed",
"Domain or instance name": "Domain or instance name" "Domain or instance name": "Domain or instance name",
"You need to enter a text": "You need to enter a text",
"Error while adding tag: {error}": "Error while adding tag: {error}"
} }

View File

@ -1636,5 +1636,7 @@
"Visit {instance_domain}": "Visiter {instance_domain}", "Visit {instance_domain}": "Visiter {instance_domain}",
"Software details: {software_details}": "Détails du logiciel : {software_details}", "Software details: {software_details}": "Détails du logiciel : {software_details}",
"Only instances with an application actor can be followed": "Seules les instances avec un acteur application peuvent être suivies", "Only instances with an application actor can be followed": "Seules les instances avec un acteur application peuvent être suivies",
"Domain or instance name": "Domaine ou nom de l'instance" "Domain or instance name": "Domaine ou nom de l'instance",
"You need to enter a text": "Vous devez entrer un texte",
"Error while adding tag: {error}": "Erreur lors de l'ajout d'un tag : {error}"
} }

View File

@ -27,6 +27,16 @@ export const orugaConfig = {
taginput: { taginput: {
itemClass: "taginput-item", itemClass: "taginput-item",
rootClass: "taginput", rootClass: "taginput",
containerClass: "taginput-container",
expandedClass: "taginput-expanded",
autocompleteClasses: {
rootClass: "taginput-autocomplete",
itemClass: "taginput-autocomplete-item",
inputClasses: {
rootClass: "taginput-input-wrapper",
inputClass: "taginput-input",
},
},
}, },
autocomplete: { autocomplete: {
rootClass: "autocomplete", rootClass: "autocomplete",
@ -57,6 +67,7 @@ export const orugaConfig = {
datepicker: { datepicker: {
iconNext: "ChevronRight", iconNext: "ChevronRight",
iconPrev: "ChevronLeft", iconPrev: "ChevronLeft",
boxClass: "datepicker-box",
}, },
modal: { modal: {
rootClass: "modal", rootClass: "modal",

View File

@ -155,6 +155,7 @@ export function iconForAddress(address: IAddress): IPOIIcon {
} }
export function addressFullName(address: IAddress): string { export function addressFullName(address: IAddress): string {
if (!address) return "";
const { name, alternativeName } = addressToPoiInfos(address); const { name, alternativeName } = addressToPoiInfos(address);
if (name && alternativeName) { if (name && alternativeName) {
return `${name}, ${alternativeName}`; return `${name}, ${alternativeName}`;

View File

@ -8,6 +8,7 @@ export interface IConversation {
id?: string; id?: string;
actor?: IActor; actor?: IActor;
lastComment?: IComment; lastComment?: IComment;
originComment?: IComment;
comments: Paginate<IComment>; comments: Paginate<IComment>;
participants: IActor[]; participants: IActor[];
updatedAt: string; updatedAt: string;

View File

@ -25,7 +25,7 @@
aria-required="true" aria-required="true"
required required
v-model="identity.name" v-model="identity.name"
@input="(event: any) => updateUsername(event.target.value)" @update:modelValue="(value: string) => updateUsername(value)"
id="identity-display-name" id="identity-display-name"
dir="auto" dir="auto"
expanded expanded
@ -740,6 +740,7 @@ const breadcrumbsLinks = computed(
); );
const updateUsername = (value: string) => { const updateUsername = (value: string) => {
if (props.isUpdate) return;
identity.value.preferredUsername = convertToUsername(value); identity.value.preferredUsername = convertToUsername(value);
}; };

View File

@ -233,7 +233,7 @@ import {
useRouteQuery, useRouteQuery,
} from "vue-use-route-query"; } from "vue-use-route-query";
import { useMutation, useQuery } from "@vue/apollo-composable"; import { useMutation, useQuery } from "@vue/apollo-composable";
import { computed, inject, ref } from "vue"; import { computed, inject, ref, watch } from "vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { useHead } from "@unhead/vue"; import { useHead } from "@unhead/vue";
import CloudQuestion from "../../../node_modules/vue-material-design-icons/CloudQuestion.vue"; import CloudQuestion from "../../../node_modules/vue-material-design-icons/CloudQuestion.vue";
@ -263,8 +263,26 @@ const { result: instancesResult } = useQuery<{
{ debounce: 500 } { debounce: 500 }
); );
watch([filterDomain, followStatus], () => {
instancePage.value = 1;
});
const instances = computed(() => instancesResult.value?.instances); const instances = computed(() => instancesResult.value?.instances);
const instancesTotal = computed(() => instancesResult.value?.instances.total);
const currentPageInstancesNumber = computed(
() => instancesResult.value?.instances.elements.length
);
// If we didn't found any instances on this page
watch(instancesTotal, (newInstancesTotal) => {
if (newInstancesTotal === 0) {
instancePage.value = 1;
} else if (currentPageInstancesNumber.value === 0) {
instancePage.value = instancePage.value - 1;
}
});
const { t } = useI18n({ useScope: "global" }); const { t } = useI18n({ useScope: "global" });
useHead({ useHead({
title: computed(() => t("Federation")), title: computed(() => t("Federation")),

View File

@ -14,7 +14,11 @@
]" ]"
/> />
<div <div
v-if="conversation.event && !isCurrentActorAuthor" v-if="
conversation.event &&
!isCurrentActorAuthor &&
isOriginCommentAuthorEventOrganizer
"
class="bg-mbz-yellow p-6 mb-3 rounded flex gap-2 items-center" class="bg-mbz-yellow p-6 mb-3 rounded flex gap-2 items-center"
> >
<Calendar :size="36" /> <Calendar :size="36" />
@ -132,7 +136,11 @@
> >
</form> </form>
<div <div
v-else-if="conversation.event" v-else-if="
conversation.event &&
!isCurrentActorAuthor &&
isOriginCommentAuthorEventOrganizer
"
class="bg-mbz-yellow p-6 rounded flex gap-2 items-center mt-3" class="bg-mbz-yellow p-6 rounded flex gap-2 items-center mt-3"
> >
<Calendar :size="36" /> <Calendar :size="36" />
@ -292,6 +300,16 @@ const isCurrentActorAuthor = computed(
currentActor.value.id !== conversation.value?.actor?.id currentActor.value.id !== conversation.value?.actor?.id
); );
const isOriginCommentAuthorEventOrganizer = computed(
() =>
conversation.value?.originComment?.actor &&
conversation.value?.event &&
[
conversation.value?.event?.organizerActor?.id,
conversation.value?.event?.attributedTo?.id,
].includes(conversation.value?.originComment?.actor.id)
);
useHead({ useHead({
title: () => title.value, title: () => title.value,
}); });

View File

@ -924,9 +924,28 @@ const handleError = (err: any) => {
console.error(err); console.error(err);
if (err.graphQLErrors !== undefined) { if (err.graphQLErrors !== undefined) {
err.graphQLErrors.forEach(({ message }: { message: string }) => { err.graphQLErrors.forEach(
notifier?.error(message); ({
}); message,
field,
}: {
message: string | { slug?: string[] }[];
field: string;
}) => {
if (
field === "tags" &&
Array.isArray(message) &&
message.some((msg) => msg.slug)
) {
const finalMsg = message.find((msg) => msg.slug?.[0]);
notifier?.error(
t("Error while adding tag: {error}", { error: finalMsg?.slug?.[0] })
);
} else if (typeof message === "string") {
notifier?.error(message);
}
}
);
} }
}; };

View File

@ -349,11 +349,10 @@ const { result: loggedUserResult } = useQuery<{ loggedUser: IUser }>(
USER_NOTIFICATIONS USER_NOTIFICATIONS
); );
const loggedUser = computed(() => loggedUserResult.value?.loggedUser); const loggedUser = computed(() => loggedUserResult.value?.loggedUser);
const feedTokens = computed( const feedTokens = computed(() =>
() => loggedUser.value?.feedTokens.filter(
loggedUser.value?.feedTokens.filter( (token: IFeedToken) => token.actor === null
(token: IFeedToken) => token.actor === null )
)
); );
const { result: webPushEnabledResult } = useQuery<{ const { result: webPushEnabledResult } = useQuery<{

View File

@ -94,7 +94,6 @@
<o-field grouped> <o-field grouped>
<o-field :label="t('City or region')" expanded label-for="setting-city"> <o-field :label="t('City or region')" expanded label-for="setting-city">
<full-address-auto-complete <full-address-auto-complete
v-if="loggedUser?.settings"
:resultType="AddressSearchType.ADMINISTRATIVE" :resultType="AddressSearchType.ADMINISTRATIVE"
v-model="address" v-model="address"
:default-text="address?.description" :default-text="address?.description"

View File

@ -6,6 +6,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier.CreateTest do
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Conversations.Conversation alias Mobilizon.Conversations.Conversation
alias Mobilizon.Discussions.Comment alias Mobilizon.Discussions.Comment
alias Mobilizon.Events.Event
alias Mobilizon.Federation.ActivityPub.Transmogrifier alias Mobilizon.Federation.ActivityPub.Transmogrifier
alias Mobilizon.Service.HTTP.ActivityPub.Mock alias Mobilizon.Service.HTTP.ActivityPub.Mock
@ -103,5 +104,57 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier.CreateTest do
{:ok, admin} = Mobilizon.Actors.get_actor_by_url("https://framapiaf.org/users/admin") {:ok, admin} = Mobilizon.Actors.get_actor_by_url("https://framapiaf.org/users/admin")
assert participant_ids == MapSet.new([actor.id, admin.id]) assert participant_ids == MapSet.new([actor.id, admin.id])
end end
test "it creates conversations for received comments if we're concerned even with reply to an event" do
actor_data = File.read!("test/fixtures/mastodon-actor.json") |> Jason.decode!()
Mock
|> expect(:call, 1, fn
%{method: :get, url: "https://framapiaf.org/users/admin"}, _opts ->
{:ok,
%Tesla.Env{
status: 200,
body:
actor_data
|> Map.put("id", "https://framapiaf.org/users/admin")
|> Map.put("preferredUsername", "admin")
}}
end)
actor = insert(:actor)
data = File.read!("test/fixtures/mastodon-post-activity-private.json") |> Jason.decode!()
data = Map.put(data, "to", [actor.url])
%Event{id: event_id, organizer_actor_id: organizer_actor_id} = event = insert(:event)
%Comment{url: reply_to_url, id: first_reply_comment_id} =
insert(:comment, visibility: :private, event: event)
object =
data["object"]
|> Map.put("to", [actor.url])
|> Map.put("inReplyTo", reply_to_url)
|> Map.put("tag", [
data["object"]["tag"]
|> hd()
|> Map.put("href", actor.url)
|> Map.put("name", Actor.preferred_username_and_domain(actor))
])
data = Map.put(data, "object", object)
{:ok, _activity,
%Conversation{
origin_comment: %Comment{visibility: :private, id: origin_comment_id},
last_comment: %Comment{visibility: :private, id: _last_comment_id},
participants: participants,
event: %Event{id: ^event_id}
}} = Transmogrifier.handle_incoming(data)
assert origin_comment_id == first_reply_comment_id
participant_ids = participants |> Enum.map(& &1.id) |> MapSet.new()
{:ok, admin} = Mobilizon.Actors.get_actor_by_url("https://framapiaf.org/users/admin")
assert participant_ids == MapSet.new([actor.id, organizer_actor_id, admin.id])
end
end end
end end

View File

@ -44,7 +44,8 @@ defmodule Mobilizon.GraphQL.Resolvers.ConversationTest do
describe "Find conversations for event" do describe "Find conversations for event" do
test "for a given event", %{conn: conn, user: user, actor: actor} do test "for a given event", %{conn: conn, user: user, actor: actor} do
event = insert(:event, organizer_actor: actor) event = insert(:event, organizer_actor: actor)
conversation = insert(:conversation, event: event) origin_comment = insert(:comment, actor: actor)
conversation = insert(:conversation, event: event, origin_comment: origin_comment)
another_comment = insert(:comment, origin_comment: conversation.origin_comment) another_comment = insert(:comment, origin_comment: conversation.origin_comment)
Discussions.update_comment(conversation.origin_comment, %{conversation_id: conversation.id}) Discussions.update_comment(conversation.origin_comment, %{conversation_id: conversation.id})

View File

@ -1,11 +1,13 @@
defmodule Mobilizon.GraphQL.Resolvers.ParticipantTest do defmodule Mobilizon.GraphQL.Resolvers.ParticipantTest do
use Mobilizon.Web.ConnCase use Mobilizon.Web.ConnCase
use Mobilizon.Tests.Helpers use Mobilizon.Tests.Helpers
use Oban.Testing, repo: Mobilizon.Storage.Repo
alias Mobilizon.Config alias Mobilizon.Actors.Actor
alias Mobilizon.Events alias Mobilizon.{Actors, Config, Conversations, Events}
alias Mobilizon.Events.{Event, EventParticipantStats, Participant} alias Mobilizon.Events.{Event, EventParticipantStats, Participant}
alias Mobilizon.GraphQL.AbsintheHelpers alias Mobilizon.GraphQL.AbsintheHelpers
alias Mobilizon.Service.Workers.LegacyNotifierBuilder
alias Mobilizon.Storage.Page alias Mobilizon.Storage.Page
import Mobilizon.Factory import Mobilizon.Factory
@ -1381,4 +1383,393 @@ defmodule Mobilizon.GraphQL.Resolvers.ParticipantTest do
assert_email_sent(to: @email) assert_email_sent(to: @email)
end end
end end
describe "Send private messages to participants" do
@send_event_private_message_mutation """
mutation SendEventPrivateMessageMutation(
$text: String!
$actorId: ID!
$eventId: ID!
$roles: [ParticipantRoleEnum]
$language: String
) {
sendEventPrivateMessage(
text: $text
actorId: $actorId
eventId: $eventId
roles: $roles
language: $language
) {
id
conversationParticipantId
actor {
id
}
lastComment {
id
text
}
originComment {
id
text
}
participants {
id
}
event {
id
uuid
title
organizerActor {
id
}
attributedTo {
id
}
}
unread
insertedAt
updatedAt
}
}
"""
setup %{conn: conn} do
user = insert(:user)
actor = insert(:actor, user: user, preferred_username: "test")
{:ok, conn: conn, actor: actor, user: user}
end
test "Without being logged-in", %{conn: conn} do
%Actor{id: actor_id} = insert(:actor)
%Event{id: event_id} = insert(:event)
res =
conn
|> AbsintheHelpers.graphql_query(
query: @send_event_private_message_mutation,
variables: %{actorId: actor_id, eventId: event_id, text: "Hello dear participants"}
)
assert hd(res["errors"])["message"] == "You need to be logged in"
end
test "With actor not allowed", %{conn: conn, actor: actor, user: user} do
%Event{id: event_id} = insert(:event)
res =
conn
|> auth_conn(user)
|> AbsintheHelpers.graphql_query(
query: @send_event_private_message_mutation,
variables: %{actorId: actor.id, eventId: event_id, text: "Hello dear participants"}
)
assert hd(res["errors"])["message"] == "You don't have permission to do this"
end
test "With actor as event organizer", %{conn: conn, actor: actor, user: user} do
%Event{id: event_id, title: event_title, uuid: event_uuid} =
event = insert(:event, organizer_actor: actor)
%Participant{actor_id: participant_actor_id} = insert(:participant, event: event)
%Participant{actor_id: participant_actor_id_2} = insert(:participant, event: event)
res =
conn
|> auth_conn(user)
|> AbsintheHelpers.graphql_query(
query: @send_event_private_message_mutation,
variables: %{actorId: actor.id, eventId: event_id, text: "Hello dear participants"}
)
assert res["errors"] == nil
assert res["data"]["sendEventPrivateMessage"]["lastComment"]["id"] ==
res["data"]["sendEventPrivateMessage"]["originComment"]["id"]
assert res["data"]["sendEventPrivateMessage"]["lastComment"]["text"] ==
"Hello dear participants"
participants_ids =
Enum.map(res["data"]["sendEventPrivateMessage"]["participants"], fn participant ->
String.to_integer(participant["id"])
end)
assert length(participants_ids) == 3
assert MapSet.new(participants_ids) ==
MapSet.new([actor.id, participant_actor_id, participant_actor_id_2])
assert res["data"]["sendEventPrivateMessage"]["actor"]["id"] == to_string(actor.id)
conversation_id = res["data"]["sendEventPrivateMessage"]["id"]
all_conversation_participants_ids = Conversations.find_all_conversations_for_event(event_id)
notified_conversation_participant_ids =
all_conversation_participants_ids
|> Enum.filter(&(&1.actor_id != actor.id))
|> Enum.map(&[&1.id, &1.actor_id])
Enum.each(notified_conversation_participant_ids, fn [pa_id, participant_actor_id] ->
assert_enqueued(
worker: LegacyNotifierBuilder,
args: %{
"author_id" => actor.id,
"object_id" => to_string(conversation_id),
"object_type" => "conversation",
"op" => "legacy_notify",
"subject" => "conversation_created",
"subject_params" => %{
"conversation_id" => String.to_integer(conversation_id),
"conversation_participant_id" => pa_id,
"conversation_text" => "Hello dear participants",
"conversation_event_id" => event_id,
"conversation_event_title" => event_title,
"conversation_event_uuid" => event_uuid
},
"type" => "conversation",
"participant" => %{
"actor_id" => participant_actor_id,
"id" => pa_id
}
}
)
end)
ignored_conversation_participant_id =
all_conversation_participants_ids
|> Enum.filter(&(&1.actor_id == actor.id))
|> Enum.map(& &1.id)
refute_enqueued(
worker: LegacyNotifierBuilder,
args: %{
"author_id" => actor.id,
"object_id" => to_string(conversation_id),
"object_type" => "conversation",
"op" => "legacy_notify",
"subject" => "conversation_created",
"subject_params" => %{
"conversation_id" => String.to_integer(conversation_id),
"conversation_participant_id" => ignored_conversation_participant_id,
"conversation_text" => "Hello dear participants",
"conversation_event_id" => event_id,
"conversation_event_title" => event_title,
"conversation_event_uuid" => event_uuid
},
"type" => "conversation",
"participant" => %{
"actor_id" => actor.id,
"id" => ignored_conversation_participant_id
}
}
)
end
test "With actor as event organizer with customized roles", %{
conn: conn,
actor: actor,
user: user
} do
%Event{id: event_id, title: event_title, uuid: event_uuid} =
event = insert(:event, organizer_actor: actor)
%Participant{actor_id: _participant_actor_id} = insert(:participant, event: event)
%Participant{actor_id: participant_actor_id_2} =
insert(:participant, event: event, role: :not_approved)
res =
conn
|> auth_conn(user)
|> AbsintheHelpers.graphql_query(
query: @send_event_private_message_mutation,
variables: %{
actorId: actor.id,
eventId: event_id,
text: "Hello dear participants",
roles: ["NOT_APPROVED"]
}
)
assert res["errors"] == nil
assert res["data"]["sendEventPrivateMessage"]["lastComment"]["id"] ==
res["data"]["sendEventPrivateMessage"]["originComment"]["id"]
assert res["data"]["sendEventPrivateMessage"]["lastComment"]["text"] ==
"Hello dear participants"
participants_ids =
Enum.map(res["data"]["sendEventPrivateMessage"]["participants"], fn participant ->
String.to_integer(participant["id"])
end)
assert length(participants_ids) == 2
assert MapSet.new(participants_ids) ==
MapSet.new([actor.id, participant_actor_id_2])
conversation_id = res["data"]["sendEventPrivateMessage"]["id"]
all_conversation_participants_ids = Conversations.find_all_conversations_for_event(event_id)
notified_conversation_participant_ids =
all_conversation_participants_ids
|> Enum.filter(&(&1.actor_id == participant_actor_id_2))
|> Enum.map(&[&1.id, &1.actor_id])
Enum.each(notified_conversation_participant_ids, fn [pa_id, participant_actor_id] ->
assert_enqueued(
worker: LegacyNotifierBuilder,
args: %{
"author_id" => actor.id,
"object_id" => to_string(conversation_id),
"object_type" => "conversation",
"op" => "legacy_notify",
"subject" => "conversation_created",
"subject_params" => %{
"conversation_id" => String.to_integer(conversation_id),
"conversation_participant_id" => pa_id,
"conversation_text" => "Hello dear participants",
"conversation_event_id" => event_id,
"conversation_event_title" => event_title,
"conversation_event_uuid" => event_uuid
},
"type" => "conversation",
"participant" => %{
"actor_id" => participant_actor_id,
"id" => pa_id
}
}
)
end)
ignored_conversation_participant_id =
all_conversation_participants_ids
|> Enum.filter(&(&1.actor_id != participant_actor_id_2))
|> Enum.map(&[&1.id, &1.actor_id])
Enum.each(ignored_conversation_participant_id, fn [pa_id, participant_actor_id] ->
refute_enqueued(
worker: LegacyNotifierBuilder,
args: %{
"author_id" => participant_actor_id,
"object_id" => to_string(conversation_id),
"object_type" => "conversation",
"op" => "legacy_notify",
"subject" => "conversation_created",
"subject_params" => %{
"conversation_id" => String.to_integer(conversation_id),
"conversation_participant_id" => pa_id,
"conversation_text" => "Hello dear participants",
"conversation_event_id" => event_id,
"conversation_event_title" => event_title,
"conversation_event_uuid" => event_uuid
},
"type" => "conversation",
"participant" => %{
"actor_id" => participant_actor_id,
"id" => pa_id
}
}
)
end)
end
test "With actor as member of group event organizer", %{conn: conn, actor: actor, user: user} do
%Actor{id: group_id} = group = insert(:group)
insert(:member, parent: group, actor: actor, role: :moderator)
%Event{id: event_id} = event = insert(:event, organizer_actor: actor, attributed_to: group)
%Participant{actor_id: participant_actor_id} = insert(:participant, event: event)
%Participant{actor_id: participant_actor_id_2} = insert(:participant, event: event)
res =
conn
|> auth_conn(user)
|> AbsintheHelpers.graphql_query(
query: @send_event_private_message_mutation,
variables: %{actorId: group_id, eventId: event_id, text: "Hello dear participants"}
)
assert res["errors"] == nil
participants_ids =
Enum.map(res["data"]["sendEventPrivateMessage"]["participants"], fn participant ->
String.to_integer(participant["id"])
end)
assert MapSet.new(participants_ids) ==
MapSet.new([group_id, participant_actor_id, participant_actor_id_2])
assert res["data"]["sendEventPrivateMessage"]["actor"]["id"] == to_string(group_id)
end
test "With event not found", %{conn: conn, actor: actor, user: user} do
res =
conn
|> auth_conn(user)
|> AbsintheHelpers.graphql_query(
query: @send_event_private_message_mutation,
variables: %{actorId: actor.id, eventId: "5019438457", text: "Hello dear participants"}
)
assert hd(res["errors"])["message"] ==
"Event not found"
end
test "With no participants matching the audience", %{conn: conn, actor: actor, user: user} do
%Event{id: event_id} = insert(:event, organizer_actor: actor)
res =
conn
|> auth_conn(user)
|> AbsintheHelpers.graphql_query(
query: @send_event_private_message_mutation,
variables: %{actorId: actor.id, eventId: event_id, text: "Hello dear participants"}
)
assert hd(res["errors"])["message"] ==
"There are no participants matching the audience you've selected."
end
end
test "With several anonymous participants", %{conn: conn, actor: actor, user: user} do
%Event{id: event_id} =
event = insert(:event, organizer_actor: actor)
{:ok, anonymous_actor} = Actors.get_or_create_internal_actor("anonymous")
refute is_nil(anonymous_actor)
insert(:participant, event: event, actor: anonymous_actor, metadata: %{email: "anon@mou.se"})
insert(:participant, event: event, actor: anonymous_actor, metadata: %{email: "other@mou.se"})
res =
conn
|> auth_conn(user)
|> AbsintheHelpers.graphql_query(
query: @send_event_private_message_mutation,
variables: %{actorId: actor.id, eventId: event_id, text: "Hello dear participants"}
)
assert res["errors"] == nil
assert res["data"]["sendEventPrivateMessage"]["lastComment"]["id"] ==
res["data"]["sendEventPrivateMessage"]["originComment"]["id"]
assert res["data"]["sendEventPrivateMessage"]["lastComment"]["text"] ==
"Hello dear participants"
participants_ids =
Enum.map(res["data"]["sendEventPrivateMessage"]["participants"], fn participant ->
String.to_integer(participant["id"])
end)
# Anonymous actor is only added once
assert length(participants_ids) == 2
assert Enum.sort(participants_ids) == Enum.sort([actor.id, anonymous_actor.id])
end
end end

View File

@ -7,6 +7,7 @@ defmodule Mobilizon.Service.Activity.ConversationTest do
alias Mobilizon.Conversations alias Mobilizon.Conversations
alias Mobilizon.Conversations.{Conversation, ConversationParticipant} alias Mobilizon.Conversations.{Conversation, ConversationParticipant}
alias Mobilizon.Discussions.Comment alias Mobilizon.Discussions.Comment
alias Mobilizon.Events.Event
alias Mobilizon.Service.Activity.Conversation, as: ConversationActivity alias Mobilizon.Service.Activity.Conversation, as: ConversationActivity
alias Mobilizon.Service.Workers.LegacyNotifierBuilder alias Mobilizon.Service.Workers.LegacyNotifierBuilder
alias Mobilizon.Users.User alias Mobilizon.Users.User
@ -15,16 +16,93 @@ defmodule Mobilizon.Service.Activity.ConversationTest do
use Oban.Testing, repo: Mobilizon.Storage.Repo use Oban.Testing, repo: Mobilizon.Storage.Repo
import Mobilizon.Factory import Mobilizon.Factory
describe "handle conversation" do describe "handle activity from event private announcement conversation" do
test "with participants" do test "when conversation initial comment author is not an organizer" do
%User{} = user = insert(:user) %User{} = user = insert(:user)
%Actor{id: actor_id} = actor = insert(:actor, user: user) %Actor{id: actor_id} = actor = insert(:actor, user: user)
%Conversation{ %Actor{} = organizer_actor = insert(:actor)
id: conversation_id,
last_comment: %Comment{actor_id: last_comment_actor_id} %Event{} = event = insert(:event)
} =
conversation = insert(:conversation, event: nil) %Comment{} = comment = insert(:comment, actor: organizer_actor)
%Conversation{id: conversation_id} =
conversation =
insert(:conversation, event: event, last_comment: comment, origin_comment: comment)
%ConversationParticipant{id: conversation_participant_actor_id} =
insert(:conversation_participant, actor: actor, conversation: conversation)
%ConversationParticipant{
id: conversation_participant_id,
actor: %Actor{id: conversation_other_participant_actor_id}
} = insert(:conversation_participant, conversation: conversation)
conversation = Conversations.get_conversation(conversation_id)
assert {:ok, _} =
ConversationActivity.insert_activity(conversation, subject: "conversation_created")
refute_enqueued(
worker: LegacyNotifierBuilder,
args: %{
"author_id" => organizer_actor.id,
"participant" => %{"actor_id" => actor_id, "id" => conversation_participant_actor_id},
"object_id" => to_string(conversation_id),
"object_type" => "conversation",
"op" => "legacy_notify",
"subject" => "conversation_created",
"subject_params" => %{
"conversation_id" => conversation_id,
"conversation_participant_id" => conversation_participant_actor_id,
"conversation_event_id" => event.id,
"conversation_event_title" => event.title,
"conversation_event_uuid" => event.uuid
},
"type" => "conversation"
}
)
refute_enqueued(
worker: LegacyNotifierBuilder,
args: %{
"author_id" => organizer_actor.id,
"participant" => %{
"actor_id" => conversation_other_participant_actor_id,
"id" => conversation_participant_id
},
"object_id" => to_string(conversation_id),
"object_type" => "conversation",
"op" => "legacy_notify",
"subject" => "conversation_created",
"subject_params" => %{
"conversation_id" => conversation_id,
"conversation_participant_id" => conversation_participant_id,
"conversation_event_id" => event.id,
"conversation_event_title" => event.title,
"conversation_event_uuid" => event.uuid
},
"type" => "conversation"
}
)
assert [] = all_enqueued()
end
test "an author who is the event organizer" do
%User{} = user = insert(:user)
%Actor{id: actor_id} = actor = insert(:actor, user: user)
%Actor{} = organizer_actor = insert(:actor)
%Event{} = event = insert(:event, organizer_actor: organizer_actor)
%Comment{} = comment = insert(:comment, actor: organizer_actor)
%Conversation{id: conversation_id} =
conversation =
insert(:conversation, event: event, last_comment: comment, origin_comment: comment)
%ConversationParticipant{id: conversation_participant_actor_id} = %ConversationParticipant{id: conversation_participant_actor_id} =
insert(:conversation_participant, actor: actor, conversation: conversation) insert(:conversation_participant, actor: actor, conversation: conversation)
@ -42,7 +120,7 @@ defmodule Mobilizon.Service.Activity.ConversationTest do
assert_enqueued( assert_enqueued(
worker: LegacyNotifierBuilder, worker: LegacyNotifierBuilder,
args: %{ args: %{
"author_id" => last_comment_actor_id, "author_id" => organizer_actor.id,
"participant" => %{"actor_id" => actor_id, "id" => conversation_participant_actor_id}, "participant" => %{"actor_id" => actor_id, "id" => conversation_participant_actor_id},
"object_id" => to_string(conversation_id), "object_id" => to_string(conversation_id),
"object_type" => "conversation", "object_type" => "conversation",
@ -50,7 +128,10 @@ defmodule Mobilizon.Service.Activity.ConversationTest do
"subject" => "conversation_created", "subject" => "conversation_created",
"subject_params" => %{ "subject_params" => %{
"conversation_id" => conversation_id, "conversation_id" => conversation_id,
"conversation_participant_id" => conversation_participant_actor_id "conversation_participant_id" => conversation_participant_actor_id,
"conversation_event_id" => event.id,
"conversation_event_title" => event.title,
"conversation_event_uuid" => event.uuid
}, },
"type" => "conversation" "type" => "conversation"
} }
@ -59,7 +140,7 @@ defmodule Mobilizon.Service.Activity.ConversationTest do
assert_enqueued( assert_enqueued(
worker: LegacyNotifierBuilder, worker: LegacyNotifierBuilder,
args: %{ args: %{
"author_id" => last_comment_actor_id, "author_id" => organizer_actor.id,
"participant" => %{ "participant" => %{
"actor_id" => conversation_other_participant_actor_id, "actor_id" => conversation_other_participant_actor_id,
"id" => conversation_participant_id "id" => conversation_participant_id
@ -70,7 +151,81 @@ defmodule Mobilizon.Service.Activity.ConversationTest do
"subject" => "conversation_created", "subject" => "conversation_created",
"subject_params" => %{ "subject_params" => %{
"conversation_id" => conversation_id, "conversation_id" => conversation_id,
"conversation_participant_id" => conversation_participant_id "conversation_participant_id" => conversation_participant_id,
"conversation_event_id" => event.id,
"conversation_event_title" => event.title,
"conversation_event_uuid" => event.uuid
},
"type" => "conversation"
}
)
end
test "an author who is member of the event organizer group" do
%User{} = user = insert(:user)
%Actor{id: actor_id} = actor = insert(:actor, user: user)
%Actor{} = organizer_group = insert(:group)
%Event{} = event = insert(:event, attributed_to: organizer_group)
%Comment{} = comment = insert(:comment, actor: organizer_group)
%Conversation{id: conversation_id} =
conversation =
insert(:conversation, event: event, last_comment: comment, origin_comment: comment)
%ConversationParticipant{id: conversation_participant_actor_id} =
insert(:conversation_participant, actor: actor, conversation: conversation)
%ConversationParticipant{
id: conversation_participant_id,
actor: %Actor{id: conversation_other_participant_actor_id}
} = insert(:conversation_participant, conversation: conversation)
conversation = Conversations.get_conversation(conversation_id)
assert {:ok, _} =
ConversationActivity.insert_activity(conversation, subject: "conversation_created")
assert_enqueued(
worker: LegacyNotifierBuilder,
args: %{
"author_id" => organizer_group.id,
"participant" => %{"actor_id" => actor_id, "id" => conversation_participant_actor_id},
"object_id" => to_string(conversation_id),
"object_type" => "conversation",
"op" => "legacy_notify",
"subject" => "conversation_created",
"subject_params" => %{
"conversation_id" => conversation_id,
"conversation_participant_id" => conversation_participant_actor_id,
"conversation_event_id" => event.id,
"conversation_event_title" => event.title,
"conversation_event_uuid" => event.uuid
},
"type" => "conversation"
}
)
assert_enqueued(
worker: LegacyNotifierBuilder,
args: %{
"author_id" => organizer_group.id,
"participant" => %{
"actor_id" => conversation_other_participant_actor_id,
"id" => conversation_participant_id
},
"object_id" => to_string(conversation_id),
"object_type" => "conversation",
"op" => "legacy_notify",
"subject" => "conversation_created",
"subject_params" => %{
"conversation_id" => conversation_id,
"conversation_participant_id" => conversation_participant_id,
"conversation_event_id" => event.id,
"conversation_event_title" => event.title,
"conversation_event_uuid" => event.uuid
}, },
"type" => "conversation" "type" => "conversation"
} }

View File

@ -11,7 +11,7 @@ defmodule Mobilizon.Service.Metadata.InstanceTest do
assert Instance.build_tags() |> Utils.stringify_tags() == assert Instance.build_tags() |> Utils.stringify_tags() ==
""" """
<title>#{title}</title><meta content="#{description}" name="description"><meta content="#{title}" property="og:title"><meta content="#{Endpoint.url()}" property="og:url"><meta content="#{description}" property="og:description"><meta content="website" property="og:type"><script type="application/ld+json">{"@context":"http://schema.org","@type":"WebSite","name":"#{title}","potentialAction":{"@type":"SearchAction","query-input":"required name=search_term","target":"#{Endpoint.url()}/search?term={search_term}"},"url":"#{Endpoint.url()}"}</script> <title>#{title}</title><meta content="#{description}" name="description"><meta content="#{title}" property="og:title"><meta content="#{Endpoint.url()}" property="og:url"><meta content="#{description}" property="og:description"><meta content="website" property="og:type"><script type="application/ld+json">{"@context":"http://schema.org","@type":"WebSite","name":"#{title}","potentialAction":{"@type":"SearchAction","query-input":"required name=search_term","target":"#{Endpoint.url()}/search?term={search_term}"},"url":"#{Endpoint.url()}"}</script>\n<link href=\"#{Endpoint.url()}/feed/instance/atom\" rel=\"alternate\" title=\"Test instance's feed\" type=\"application/atom+xml\"><link href=\"#{Endpoint.url()}/feed/instance/ics\" rel=\"alternate\" title=\"Test instance's feed\" type=\"text/calendar\">\
""" """
end end
end end

View File

@ -4,6 +4,7 @@ defmodule Mobilizon.Service.Workers.LegacyNotifierBuilderTest do
""" """
alias Mobilizon.Activities.Activity alias Mobilizon.Activities.Activity
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Discussions.{Comment, Discussion} alias Mobilizon.Discussions.{Comment, Discussion}
alias Mobilizon.Events.Event alias Mobilizon.Events.Event
@ -15,6 +16,7 @@ defmodule Mobilizon.Service.Workers.LegacyNotifierBuilderTest do
use Mobilizon.Tests.Helpers use Mobilizon.Tests.Helpers
import Mox import Mox
import Mobilizon.Factory import Mobilizon.Factory
import Mobilizon.Tests.SwooshAssertions
setup_all do setup_all do
Mox.defmock(NotifierMock, for: Mobilizon.Service.Notifier) Mox.defmock(NotifierMock, for: Mobilizon.Service.Notifier)
@ -42,7 +44,7 @@ defmodule Mobilizon.Service.Workers.LegacyNotifierBuilderTest do
"op" => "legacy_notify" "op" => "legacy_notify"
} }
@announcement %{ @public_announcement %{
"type" => "comment", "type" => "comment",
"subject" => "participation_event_comment", "subject" => "participation_event_comment",
"object_type" => "comment", "object_type" => "comment",
@ -50,6 +52,14 @@ defmodule Mobilizon.Service.Workers.LegacyNotifierBuilderTest do
"op" => "legacy_notify" "op" => "legacy_notify"
} }
@private_announcement %{
"type" => "conversation",
"subject" => "conversation_created",
"object_type" => "conversation",
"inserted_at" => DateTime.utc_now(),
"op" => "legacy_notify"
}
setup :verify_on_exit! setup :verify_on_exit!
describe "Generates a comment mention notification " do describe "Generates a comment mention notification " do
@ -138,7 +148,7 @@ defmodule Mobilizon.Service.Workers.LegacyNotifierBuilderTest do
%Comment{id: comment_id} = insert(:comment, event: event, actor: actor) %Comment{id: comment_id} = insert(:comment, event: event, actor: actor)
args = args =
Map.merge(@announcement, %{ Map.merge(@public_announcement, %{
"subject_params" => %{ "subject_params" => %{
"event_uuid" => uuid, "event_uuid" => uuid,
"event_title" => title, "event_title" => title,
@ -177,7 +187,7 @@ defmodule Mobilizon.Service.Workers.LegacyNotifierBuilderTest do
insert(:participant, event: event, actor: actor2) insert(:participant, event: event, actor: actor2)
args = args =
Map.merge(@announcement, %{ Map.merge(@public_announcement, %{
"subject_params" => %{ "subject_params" => %{
"event_uuid" => uuid, "event_uuid" => uuid,
"event_title" => title, "event_title" => title,
@ -334,4 +344,91 @@ defmodule Mobilizon.Service.Workers.LegacyNotifierBuilderTest do
assert :ok == LegacyNotifierBuilder.perform(%Oban.Job{args: args}) assert :ok == LegacyNotifierBuilder.perform(%Oban.Job{args: args})
end end
end end
describe "Generates a private event announcement notification" do
test "sends emails to target users" do
user1 = insert(:user, email: "user1@do.main")
actor1 = insert(:actor, user: user1)
user2 = insert(:user, email: "user2@do.main")
actor2 = insert(:actor, user: user2)
event = insert(:event)
comment = insert(:comment, actor: actor2, visibility: :private)
conversation =
insert(:conversation, event: event, last_comment: comment, origin_comment: comment)
conversation_participant =
insert(:conversation_participant, conversation: conversation, actor: actor1)
args =
Map.merge(@private_announcement, %{
"subject_params" => %{
"conversation_id" => conversation.id,
"conversation_participant_id" => conversation_participant.id,
"conversation_text" => conversation.last_comment.text,
"conversation_event_id" => event.id,
"conversation_event_title" => event.title,
"conversation_event_uuid" => event.uuid
},
"author_id" => conversation.last_comment.actor.id,
"object_id" => conversation.last_comment.id,
"participant" => %{
"actor_id" => actor1.id,
"id" => conversation_participant.id
}
})
LegacyNotifierBuilder.perform(%Oban.Job{args: args})
assert_email_sent(%Swoosh.Email{to: [{"", "user1@do.main"}]})
refute_email_sent(%Swoosh.Email{to: [{"", "user2@do.main"}]})
refute_email_sent(%Swoosh.Email{to: [{"", "user1@do.main"}]})
end
test "sends emails to anonymous participants" do
{:ok, anonymous_actor} = Actors.get_or_create_internal_actor("anonymous")
refute is_nil(anonymous_actor)
user2 = insert(:user, email: "user2@do.main")
actor2 = insert(:actor, user: user2)
event = insert(:event)
comment = insert(:comment, actor: actor2, visibility: :private)
insert(:participant,
event: event,
actor: anonymous_actor,
metadata: %{email: "anon@mou.se"}
)
conversation =
insert(:conversation, event: event, last_comment: comment, origin_comment: comment)
conversation_participant =
insert(:conversation_participant, conversation: conversation, actor: anonymous_actor)
args =
Map.merge(@private_announcement, %{
"subject_params" => %{
"conversation_id" => conversation.id,
"conversation_participant_id" => conversation_participant.id,
"conversation_text" => conversation.last_comment.text,
"conversation_event_id" => event.id,
"conversation_event_title" => event.title,
"conversation_event_uuid" => event.uuid
},
"author_id" => conversation.last_comment.actor.id,
"object_id" => conversation.last_comment.id,
"participant" => %{
"actor_id" => anonymous_actor.id,
"id" => conversation_participant.id
}
})
LegacyNotifierBuilder.perform(%Oban.Job{args: args})
assert_email_sending(%Swoosh.Email{to: [{"", "anon@mou.se"}]}, 10_000)
refute_email_sent(%Swoosh.Email{to: [{"", "user2@do.main"}]})
# Because of timeouts, can't do that currently
# refute_email_sent(%Swoosh.Email{to: [{"", "anon@mou.se"}]})
end
end
end end

View File

@ -0,0 +1,91 @@
# The following module is taken from this issue
# https://github.com/swoosh/swoosh/issues/488#issuecomment-1671224765
defmodule Mobilizon.Tests.SwooshAssertions do
@moduledoc ~S"""
Assertions for emails.
The assertions provided by this module work by pattern matching
against all emails received by the test process against the
`Swoosh.Email` struct. For example:
assert_email_sent %{subject: "You got a message"}
If you want to be additionally explicit, you might:
assert_email_sent %Swoosh.Email{subject: "You got a message"}
If emails are being sent concurrently, you can use `assert_email_sending/2`:
assert_email_sending %{subject: "You got a message"}
Both functions will return the matched email if the assertion succeeds.
You can then perform further matches on it:
email = assert_email_sent %Swoosh.Email{subject: "You got a message"}
assert email.from == {"MyApp", "no-reply@example.com"}
Using pattern matching imposes two limitations. The first one is that you
must match precisely the Swoosh.Email structure. For example, the following
will not work:
assert_email_sent %{to: "foobar@example.com"}
That's because `Swoosh.Email` keeps the field as a list. This will work:
assert_email_sent %{to: [{"FooBar", "foobar@example.com"}]}
You are also not allowed to have interpolations. For example, the following
will not work:
assert_email_sent %{
subject: "You have been invited to #{org.name}",
to: [{user.name, user.email}]
}
However, you can rely on pattern matching and rewrite it as:
email = assert_email_sent %{subject: "You have been invited to " <> org_name}
assert org_name == org.name
assert email.to == [{user.name, user.email}]
"""
@doc """
Matches an email has been sent.
See moduledoc for more information.
"""
defmacro assert_email_sent(pattern) do
quote do
{:email, email} = assert_received({:email, unquote(pattern)})
email
end
end
@doc """
Matches an email is sending (within a timeout).
See moduledoc for more information.
"""
defmacro assert_email_sending(
pattern,
timeout \\ Application.fetch_env!(:ex_unit, :assert_receive_timeout)
) do
quote do
{:email, email} = assert_receive({:email, unquote(pattern)}, unquote(timeout))
email
end
end
@doc """
Refutes an email matching pattern has been sent.
The opposite of `assert_email_sent`.
"""
defmacro refute_email_sent(pattern) do
quote do
refute_received({:email, unquote(pattern)})
end
end
end

View File

@ -1,5 +0,0 @@
// vetur.config.js
/** @type {import('vls').VeturConfig} */
module.exports = {
projects: ["./js"],
};