From 02d1cea2d798dcf5c658efac1d1ff0f29eab01a4 Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Wed, 27 Feb 2019 16:28:09 +0100 Subject: [PATCH] Add cached RSS feeds for actors endpoints --- lib/mobilizon/actors/actor.ex | 15 +++ lib/mobilizon/actors/actors.ex | 58 ---------- lib/mobilizon/application.ex | 1 + .../controllers/activity_pub_controller.ex | 4 + .../controllers/feed_controller.ex | 105 ++++++++++++++++++ lib/mobilizon_web/router.ex | 12 +- mix.exs | 9 +- mix.lock | 10 ++ priv/gettext/default.pot | 59 ++++++++++ priv/gettext/en/LC_MESSAGES/default.po | 63 +++++++++++ priv/gettext/errors.pot | 1 - priv/gettext/fr_FR/LC_MESSAGES/default.po | 63 +++++++++++ priv/gettext/fr_FR/LC_MESSAGES/errors.po | 87 +++++++++++++++ .../controllers/feed_controller_test.exs | 37 ++++++ 14 files changed, 462 insertions(+), 62 deletions(-) create mode 100644 lib/mobilizon_web/controllers/feed_controller.ex create mode 100644 priv/gettext/default.pot create mode 100644 priv/gettext/en/LC_MESSAGES/default.po create mode 100644 priv/gettext/fr_FR/LC_MESSAGES/default.po create mode 100644 priv/gettext/fr_FR/LC_MESSAGES/errors.po create mode 100644 test/mobilizon_web/controllers/feed_controller_test.exs diff --git a/lib/mobilizon/actors/actor.ex b/lib/mobilizon/actors/actor.ex index 758ef607..54844aff 100644 --- a/lib/mobilizon/actors/actor.ex +++ b/lib/mobilizon/actors/actor.ex @@ -354,6 +354,9 @@ defmodule Mobilizon.Actors.Actor do end end + @doc """ + Return the preferred_username with the eventual @domain suffix if it's a distant actor + """ @spec actor_acct_from_actor(struct()) :: String.t() def actor_acct_from_actor(%Actor{preferred_username: preferred_username, domain: domain}) do if is_nil(domain) do @@ -362,4 +365,16 @@ defmodule Mobilizon.Actors.Actor do "#{preferred_username}@#{domain}" end end + + @doc """ + Returns the display name if available, or the preferred_username (with the eventual @domain suffix if it's a distant actor). + """ + @spec display_name(struct()) :: String.t() + def display_name(%Actor{name: name} = actor) do + case name do + nil -> actor_acct_from_actor(actor) + "" -> actor_acct_from_actor(actor) + name -> name + end + end end diff --git a/lib/mobilizon/actors/actors.ex b/lib/mobilizon/actors/actors.ex index 7b29fd91..92a4e40a 100644 --- a/lib/mobilizon/actors/actors.ex +++ b/lib/mobilizon/actors/actors.ex @@ -612,67 +612,9 @@ defmodule Mobilizon.Actors do with {:ok, %User{} = user} <- %User{} |> User.registration_changeset(args) |> Mobilizon.Repo.insert() do {:ok, user} - # else - # {:error, %Ecto.Changeset{} = changeset} -> - # {:error, Ecto.Changeset.traverse_errors(changeset, fn {msg, opts} -> - # Enum.reduce(opts, msg, fn {key, value}, acc -> - # String.replace(acc, "%{#{key}}", to_string(value)) - # end) - # end)} end end - # @spec register(map()) :: {:ok, Actor.t()} | {:error, String.t()} - # def register(%{email: email, password: password, username: username}) do - # with avatar <- gravatar(email), - # user_changeset <- - # User.registration_changeset(%User{}, %{ - # email: email, - # password: password, - # default_actor: %{ - # preferred_username: username, - # domain: nil, - # keys: create_keys(), - # avatar_url: avatar - # } - # }), - # {:ok, %User{default_actor: %Actor{} = actor, id: user_id} = user} <- - # Mobilizon.Repo.insert(user_changeset), - # {:ok, %Actor{} = _actor} <- update_actor(actor, %{user_id: user_id}) do - # {:ok, Repo.preload(user, [:actors])} - # else - # {:error, %Ecto.Changeset{} = changeset} -> - # handle_actor_user_changeset(changeset) - # end - # end - - # @spec handle_actor_user_changeset(Ecto.Changeset.t()) :: {:error, String.t()} - # defp handle_actor_user_changeset(changeset) do - # changeset = - # Ecto.Changeset.traverse_errors(changeset, fn - # {msg, _opts} -> msg - # msg -> msg - # end) - - # email_msg = Map.get(changeset, :email) || [:empty_email] - # {:error, hd(email_msg)} - # end - - # @spec gravatar(String.t()) :: String.t() | nil - # defp gravatar(nil), do: nil - - # defp gravatar(email) do - # avatar_url = gravatar_url(email, default: "404") - - # case HTTPoison.get(avatar_url) do - # {:ok, %HTTPoison.Response{status_code: 200}} -> - # avatar_url - - # _ -> - # nil - # end - # end - @doc """ Create a new person actor """ diff --git a/lib/mobilizon/application.ex b/lib/mobilizon/application.ex index 68ad7fe4..23019a31 100644 --- a/lib/mobilizon/application.ex +++ b/lib/mobilizon/application.ex @@ -17,6 +17,7 @@ defmodule Mobilizon.Application do supervisor(MobilizonWeb.Endpoint, []), # Start your own worker by calling: Mobilizon.Worker.start_link(arg1, arg2, arg3) # worker(Mobilizon.Worker, [arg1, arg2, arg3]), + worker(Cachex, [:mobilizon, []]), worker(Guardian.DB.Token.SweeperServer, []), worker(Mobilizon.Service.Federator, []) ] diff --git a/lib/mobilizon_web/controllers/activity_pub_controller.ex b/lib/mobilizon_web/controllers/activity_pub_controller.ex index c7c6a963..631091bc 100644 --- a/lib/mobilizon_web/controllers/activity_pub_controller.ex +++ b/lib/mobilizon_web/controllers/activity_pub_controller.ex @@ -21,6 +21,10 @@ defmodule MobilizonWeb.ActivityPubController do "application/activity+json, application/ld+json" ] + def actor(conn, %{"name" => _name, "_format" => "atom"} = params) do + MobilizonWeb.FeedController.actor(conn, params) + end + def actor(conn, %{"name" => name}) do with %Actor{} = actor <- Actors.get_local_actor_by_name(name) do if conn |> get_req_header("accept") |> is_ap_header() do diff --git a/lib/mobilizon_web/controllers/feed_controller.ex b/lib/mobilizon_web/controllers/feed_controller.ex new file mode 100644 index 00000000..30c7b471 --- /dev/null +++ b/lib/mobilizon_web/controllers/feed_controller.ex @@ -0,0 +1,105 @@ +defmodule MobilizonWeb.FeedController do + @moduledoc """ + Controller to serve RSS, ATOM and iCal Feeds + """ + use MobilizonWeb, :controller + + alias Mobilizon.Actors + alias Mobilizon.Actors.Actor + alias Mobilizon.Events + alias Mobilizon.Events.Event + alias Atomex.{Feed, Entry} + import MobilizonWeb.Gettext + + @version Mix.Project.config()[:version] + def version(), do: @version + + def actor(conn, %{"name" => name, "_format" => format}) when format in ["atom"] do + name = String.replace_suffix(name, ".atom", "") + + with {status, data} when status in [:ok, :commit] <- + Cachex.fetch(:mobilizon, "actor_" <> format <> "_" <> name, &create_cache/1) do + conn + |> put_resp_content_type("application/atom+xml") + |> send_resp(200, data) + else + _err -> + send_resp(conn, 404, "Not found") + end + end + + @spec create_cache(String.t()) :: {:commit, String.t()} | {:ignore, any()} + defp create_cache(key) do + with ["actor", type, name] <- String.split(key, "_", parts: 3), + {:ok, res} <- fetch_actor_event_feed(type, name) do + {:commit, res} + else + err -> + {:ignore, err} + end + end + + @spec fetch_actor_event_feed(String.t(), String.t()) :: String.t() + defp fetch_actor_event_feed(type, name) do + with %Actor{} = actor <- Actors.get_local_actor_by_name(name), + {:ok, events, _count} <- Events.get_public_events_for_actor(actor) do + {:ok, build_actor_feed(actor, events, type)} + else + err -> + {:error, err} + end + end + + @spec build_actor_feed(Actor.t(), list(), String.t()) :: String.t() + defp build_actor_feed(%Actor{} = actor, events, type) do + display_name = Actor.display_name(actor) + + # Title uses default instance language + feed = + Feed.new( + actor.url <> ".rss", + DateTime.utc_now(), + gettext("%{actor}'s public events feed", actor: display_name) + ) + |> Feed.author(display_name, uri: actor.url) + |> Feed.link(actor.url <> "." <> type, rel: "self") + |> Feed.link(actor.url, rel: "alternate") + |> Feed.generator("Mobilizon", uri: "https://joinmobilizon.org", version: version()) + |> Feed.entries(Enum.map(events, &get_entry/1)) + + feed = if actor.avatar_url, do: Feed.icon(feed, actor.avatar_url), else: feed + + feed = + if actor.banner_url, + do: Feed.logo(feed, actor.banner_url), + else: feed + + feed + |> Feed.build() + |> Atomex.generate_document() + end + + defp get_entry(%Event{} = event) do + with {:ok, html, []} <- Earmark.as_html(event.description) do + entry = + Entry.new(event.url, event.inserted_at, event.title) + |> Entry.link(event.url, rel: "alternate", type: "text/html") + |> Entry.content({:cdata, html}, type: "html") + + entry = if event.publish_at, do: Entry.published(entry, event.publish_at), else: entry + + # Add tags + entry = + event.tags + |> Enum.map(& &1.title) + |> Enum.uniq() + |> Enum.reduce(entry, fn tag, acc -> Entry.category(acc, tag) end) + + Entry.build(entry) + else + {:error, _html, error_messages} -> + require Logger + Logger.error("Unable to produce HTML for Markdown", details: inspect(error_messages)) + end + end +end diff --git a/lib/mobilizon_web/router.ex b/lib/mobilizon_web/router.ex index 8dde8992..129ae2cb 100644 --- a/lib/mobilizon_web/router.ex +++ b/lib/mobilizon_web/router.ex @@ -22,6 +22,11 @@ defmodule MobilizonWeb.Router do plug(:accepts, ["activity-json", "html"]) end + pipeline :activity_pub_rss do + plug(TrailingFormatPlug) + plug(:accepts, ["activity-json", "html", "atom"]) + end + pipeline :browser do plug(:accepts, ["html"]) plug(:fetch_session) @@ -52,9 +57,14 @@ defmodule MobilizonWeb.Router do end scope "/", MobilizonWeb do - pipe_through(:activity_pub) + pipe_through(:activity_pub_rss) get("/@:name", ActivityPubController, :actor) + end + + scope "/", MobilizonWeb do + pipe_through(:activity_pub) + get("/@:name/outbox", ActivityPubController, :outbox) get("/@:name/following", ActivityPubController, :following) get("/@:name/followers", ActivityPubController, :followers) diff --git a/mix.exs b/mix.exs index ae77b6a8..747258f5 100644 --- a/mix.exs +++ b/mix.exs @@ -35,7 +35,7 @@ defmodule Mobilizon.Mixfile do def application do [ mod: {Mobilizon.Application, []}, - extra_applications: [:logger, :runtime_tools, :guardian, :bamboo, :geolix, :crypto] + extra_applications: [:logger, :runtime_tools, :guardian, :bamboo, :geolix, :crypto, :cachex] ] end @@ -85,6 +85,10 @@ defmodule Mobilizon.Mixfile do {:arc, "~> 0.11.0"}, {:arc_ecto, "~> 0.11.0"}, {:plug_cowboy, "~> 2.0"}, + {:atomex, "0.3.0"}, + {:cachex, "~> 3.1"}, + {:trailing_format_plug, "~> 0.0.5"}, + {:earmark, "~> 1.3.1"}, # Dev and test dependencies {:phoenix_live_reload, "~> 1.2", only: :dev}, {:ex_machina, "~> 2.2", only: [:dev, :test]}, @@ -95,7 +99,8 @@ defmodule Mobilizon.Mixfile do {:dialyxir, "~> 1.0.0-rc.4", only: [:dev], runtime: false}, {:exvcr, "~> 0.10", only: :test}, {:credo, "~> 1.0.0", only: [:dev, :test], runtime: false}, - {:mock, "~> 0.3.0", only: :test} + {:mock, "~> 0.3.0", only: :test}, + {:feeder_ex, "~> 1.1", only: :test} ] end diff --git a/mix.lock b/mix.lock index c065b8b1..6169232e 100644 --- a/mix.lock +++ b/mix.lock @@ -6,10 +6,12 @@ "arc": {:hex, :arc, "0.11.0", "ac7a0cc03035317b6fef9fe94c97d7d9bd183a3e7ce1606aa0c175cfa8d1ba6d", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:ex_aws_s3, "~> 2.0", [hex: :ex_aws_s3, repo: "hexpm", optional: true]}, {:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.1", [hex: :poison, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.6", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm"}, "arc_ecto": {:hex, :arc_ecto, "0.11.1", "27aedf8c236b2097eed09d96f4ae73b43eb4c042a0e2ae42d44bf644cf16115c", [:mix], [{:arc, "~> 0.11.0", [hex: :arc, repo: "hexpm", optional: false]}, {:ecto, "~> 2.1 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm"}, "argon2_elixir": {:hex, :argon2_elixir, "2.0.0", "e3539f441930d4c8296e36024168526626351c1f2c2df97cfd50f4e90b15386a", [:make, :mix], [{:comeonin, "~> 5.0", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm"}, + "atomex": {:hex, :atomex, "0.3.0", "19b5d1a2aef8706dbd307385f7d5d9f6f273869226d317492c396c7bacf26402", [:mix], [{:xml_builder, "~> 2.0.0", [hex: :xml_builder, repo: "hexpm", optional: false]}], "hexpm"}, "bamboo": {:hex, :bamboo, "1.2.0", "8aebd24f7c606c32d0163c398004a11608ca1028182a169b2e527793bfab7561", [:mix], [{:hackney, ">= 1.13.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, ">= 1.5.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"}, "bamboo_smtp": {:hex, :bamboo_smtp, "1.6.0", "0a3607b77f22554af58c547350c1c73ebba6f4fb2c4bd0b11713ab5b4081588f", [:mix], [{:bamboo, "~> 1.0", [hex: :bamboo, repo: "hexpm", optional: false]}, {:gen_smtp, "~> 0.12.0", [hex: :gen_smtp, repo: "hexpm", optional: false]}], "hexpm"}, "base64url": {:hex, :base64url, "0.0.1", "36a90125f5948e3afd7be97662a1504b934dd5dac78451ca6e9abf85a10286be", [:rebar], [], "hexpm"}, "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"}, + "cachex": {:hex, :cachex, "3.1.3", "86ed0669ea4b2f3e3982dbb5c6ca9e0964e46738e572c9156f22ceb75f57c336", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:jumper, "~> 1.0", [hex: :jumper, repo: "hexpm", optional: false]}, {:sleeplocks, "~> 1.1", [hex: :sleeplocks, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm"}, "certifi": {:hex, :certifi, "2.4.2", "75424ff0f3baaccfd34b1214184b6ef616d89e420b258bb0a5ea7d7bc628f7f0", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"}, "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm"}, "comeonin": {:hex, :comeonin, "5.0.0", "e87716d3b1c31e56312f6a1545a5548cdc80376cff5025fe3b12be2046934837", [:mix], [], "hexpm"}, @@ -29,6 +31,7 @@ "ecto_sql": {:hex, :ecto_sql, "3.0.5", "7e44172b4f7aca4469f38d7f6a3da394dbf43a1bcf0ca975e958cb957becd74e", [:mix], [{:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.0.6", [hex: :ecto, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.9.1", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.14.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.3.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"}, "elixir_make": {:hex, :elixir_make, "0.5.2", "96a28c79f5b8d34879cd95ebc04d2a0d678cfbbd3e74c43cb63a76adf0ee8054", [:mix], [], "hexpm"}, "erlex": {:hex, :erlex, "0.2.1", "cee02918660807cbba9a7229cae9b42d1c6143b768c781fa6cee1eaf03ad860b", [:mix], [], "hexpm"}, + "eternal": {:hex, :eternal, "1.2.1", "d5b6b2499ba876c57be2581b5b999ee9bdf861c647401066d3eeed111d096bc4", [:mix], [], "hexpm"}, "ex_crypto": {:hex, :ex_crypto, "0.10.0", "af600a89b784b36613a989da6e998c1b200ff1214c3cfbaf8deca4aa2f0a1739", [:mix], [{:poison, ">= 2.0.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, "ex_doc": {:hex, :ex_doc, "0.19.3", "3c7b0f02851f5fc13b040e8e925051452e41248f685e40250d7e40b07b9f8c10", [:mix], [{:earmark, "~> 1.2", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.10", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"}, "ex_ical": {:hex, :ex_ical, "0.2.0", "4b928b554614704016cc0c9ee226eb854da9327a1cc460457621ceacb1ac29a6", [:mix], [{:timex, "~> 3.1", [hex: :timex, repo: "hexpm", optional: false]}], "hexpm"}, @@ -39,6 +42,8 @@ "exgravatar": {:hex, :exgravatar, "2.0.1", "66d595c7d63dd6bbac442c5542a724375ae29144059c6fe093e61553850aace4", [:mix], [], "hexpm"}, "exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm"}, "exvcr": {:hex, :exvcr, "0.10.3", "1ae3b97560430acfa88ebc737c85b2b7a9dbacd8a2b26789a19718b51ae3522c", [:mix], [{:exactor, "~> 2.2", [hex: :exactor, repo: "hexpm", optional: false]}, {:exjsx, "~> 4.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:httpoison, "~> 1.0", [hex: :httpoison, repo: "hexpm", optional: true]}, {:httpotion, "~> 3.1", [hex: :httpotion, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:meck, "~> 0.8", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm"}, + "feeder": {:hex, :feeder, "2.2.4", "56ec535cf2f79719bc53b5c2abe5f6cf481fc01e5ae6229ab7cc829644f039ec", [:make], [], "hexpm"}, + "feeder_ex": {:hex, :feeder_ex, "1.1.0", "0be3732255cdb45dec949e0ede6852b5261c9ff173360e8274a6ac65183b2b55", [:mix], [{:feeder, "~> 2.2", [hex: :feeder, repo: "hexpm", optional: false]}], "hexpm"}, "file_system": {:hex, :file_system, "0.2.6", "fd4dc3af89b9ab1dc8ccbcc214a0e60c41f34be251d9307920748a14bf41f1d3", [:mix], [], "hexpm"}, "gen_smtp": {:hex, :gen_smtp, "0.12.0", "97d44903f5ca18ca85cb39aee7d9c77e98d79804bbdef56078adcf905cb2ef00", [:rebar3], [], "hexpm"}, "geo": {:hex, :geo, "3.1.0", "727e005262430d037e870ff364e65d80ca5ca21d5ac8eddd57a1ada72c3f83b0", [:mix], [], "hexpm"}, @@ -56,6 +61,7 @@ "jose": {:hex, :jose, "1.9.0", "4167c5f6d06ffaebffd15cdb8da61a108445ef5e85ab8f5a7ad926fdf3ada154", [:mix, :rebar3], [{:base64url, "~> 0.0.1", [hex: :base64url, repo: "hexpm", optional: false]}], "hexpm"}, "json_ld": {:hex, :json_ld, "0.3.0", "92f508ca831b9e4530e3e6c950976fdafcf26323e6817c325b3e1ee78affc4bd", [:mix], [{:jason, "~> 1.1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:rdf, "~> 0.5", [hex: :rdf, repo: "hexpm", optional: false]}], "hexpm"}, "jsx": {:hex, :jsx, "2.8.3", "a05252d381885240744d955fbe3cf810504eb2567164824e19303ea59eef62cf", [:mix, :rebar3], [], "hexpm"}, + "jumper": {:hex, :jumper, "1.0.1", "3c00542ef1a83532b72269fab9f0f0c82bf23a35e27d278bfd9ed0865cecabff", [:mix], [], "hexpm"}, "makeup": {:hex, :makeup, "0.8.0", "9cf32aea71c7fe0a4b2e9246c2c4978f9070257e5c9ce6d4a28ec450a839b55f", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, "makeup_elixir": {:hex, :makeup_elixir, "0.13.0", "be7a477997dcac2e48a9d695ec730b2d22418292675c75aa2d34ba0909dcdeda", [:mix], [{:makeup, "~> 0.8", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"}, "meck": {:hex, :meck, "0.8.13", "ffedb39f99b0b99703b8601c6f17c7f76313ee12de6b646e671e3188401f7866", [:rebar3], [], "hexpm"}, @@ -81,10 +87,14 @@ "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm"}, "rdf": {:hex, :rdf, "0.5.4", "57e09d4adfe7646fe0c3514b703b76eaf29d537b250b36abae75e66d7e5920cf", [:mix], [{:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm"}, "rsa_ex": {:hex, :rsa_ex, "0.4.0", "e28dd7dc5236e156df434af0e4aa822384c8866c928e17b785d4edb7c253b558", [:mix], [], "hexpm"}, + "sleeplocks": {:hex, :sleeplocks, "1.1.1", "3d462a0639a6ef36cc75d6038b7393ae537ab394641beb59830a1b8271faeed3", [:rebar3], [], "hexpm"}, "slugger": {:hex, :slugger, "0.3.0", "efc667ab99eee19a48913ccf3d038b1fb9f165fa4fbf093be898b8099e61b6ed", [:mix], [], "hexpm"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.4", "f0eafff810d2041e93f915ef59899c923f4568f4585904d010387ed74988e77b", [:make, :mix, :rebar3], [], "hexpm"}, "telemetry": {:hex, :telemetry, "0.3.0", "099a7f3ce31e4780f971b4630a3c22ec66d22208bc090fe33a2a3a6a67754a73", [:rebar3], [], "hexpm"}, "timex": {:hex, :timex, "3.5.0", "b0a23167da02d0fe4f1a4e104d1f929a00d348502b52432c05de875d0b9cffa5", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"}, + "trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, "tzdata": {:hex, :tzdata, "0.5.19", "7962a3997bf06303b7d1772988ede22260f3dae1bf897408ebdac2b4435f4e6a", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm"}, + "unsafe": {:hex, :unsafe, "1.0.1", "a27e1874f72ee49312e0a9ec2e0b27924214a05e3ddac90e91727bc76f8613d8", [:mix], [], "hexpm"}, + "xml_builder": {:hex, :xml_builder, "2.0.0", "371ed27bb63bf0598dbaf3f0c466e5dc7d16cb4ecb68f06a67f953654062e21b", [:mix], [], "hexpm"}, } diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot new file mode 100644 index 00000000..a9ac7b9c --- /dev/null +++ b/priv/gettext/default.pot @@ -0,0 +1,59 @@ +## This file is a PO Template file. +## +## `msgid`s here are often extracted from source code. +## Add new translations manually only if they're dynamic +## translations that can't be statically extracted. +## +## Run `mix gettext.extract` to bring this file up to +## date. Leave `msgstr`s empty as changing them here as no +## effect: edit them in PO (`.po`) files instead. +#, elixir-format +#: lib/mobilizon_web/controllers/feed_controller.ex:59 +msgid "%{actor}'s public events feed" +msgstr "" + +#, elixir-format +#: lib/mobilizon_web/templates/email/email.html.eex:8 +#: lib/mobilizon_web/templates/email/email.text.eex:3 +msgid "An email sent by Mobilizon on %{instance}." +msgstr "" + +#, elixir-format +#: lib/mobilizon_web/templates/email/registration_confirmation.html.eex:1 +#: lib/mobilizon_web/templates/email/registration_confirmation.text.eex:1 +msgid "Confirm the email address" +msgstr "" + +#, elixir-format +#: lib/mobilizon_web/templates/email/password_reset.html.eex:3 +#: lib/mobilizon_web/templates/email/password_reset.text.eex:7 +msgid "If you didn't request this, please ignore this email. Your password won't change until you access the link below and create a new one." +msgstr "" + +#, elixir-format +#: lib/mobilizon/email/user.ex:19 +msgid "Mobilizon: Confirmation instructions for %{instance}" +msgstr "" + +#, elixir-format +#: lib/mobilizon/email/user.ex:34 +msgid "Mobilizon: Reset your password on %{instance} instructions" +msgstr "" + +#, elixir-format +#: lib/mobilizon_web/templates/email/password_reset.html.eex:1 +#: lib/mobilizon_web/templates/email/password_reset.text.eex:1 +msgid "Password reset" +msgstr "" + +#, elixir-format +#: lib/mobilizon_web/templates/email/registration_confirmation.html.eex:2 +#: lib/mobilizon_web/templates/email/registration_confirmation.text.eex:5 +msgid "You created an account on %{host} with this email address. You are one click away from activating it. If this wasn't you, please ignore this email." +msgstr "" + +#, elixir-format +#: lib/mobilizon_web/templates/email/password_reset.html.eex:2 +#: lib/mobilizon_web/templates/email/password_reset.text.eex:5 +msgid "You requested a new password for your account on %{host}." +msgstr "" diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po new file mode 100644 index 00000000..7a19a514 --- /dev/null +++ b/priv/gettext/en/LC_MESSAGES/default.po @@ -0,0 +1,63 @@ +## `msgid`s in this file come from POT (.pot) files. +## +## Do not add, change, or remove `msgid`s manually here as +## they're tied to the ones in the corresponding POT file +## (with the same domain). +## +## Use `mix gettext.extract --merge` or `mix gettext.merge` +## to merge POT files into PO files. +msgid "" +msgstr "" +"Language: en\n" +"Plural-Forms: nplurals=2\n" + +#, elixir-format +#: lib/mobilizon_web/controllers/feed_controller.ex:59 +msgid "%{actor}'s public events feed" +msgstr "" + +#, elixir-format +#: lib/mobilizon_web/templates/email/email.html.eex:8 +#: lib/mobilizon_web/templates/email/email.text.eex:3 +msgid "An email sent by Mobilizon on %{instance}." +msgstr "" + +#, elixir-format +#: lib/mobilizon_web/templates/email/registration_confirmation.html.eex:1 +#: lib/mobilizon_web/templates/email/registration_confirmation.text.eex:1 +msgid "Confirm the email address" +msgstr "" + +#, elixir-format +#: lib/mobilizon_web/templates/email/password_reset.html.eex:3 +#: lib/mobilizon_web/templates/email/password_reset.text.eex:7 +msgid "If you didn't request this, please ignore this email. Your password won't change until you access the link below and create a new one." +msgstr "" + +#, elixir-format +#: lib/mobilizon/email/user.ex:19 +msgid "Mobilizon: Confirmation instructions for %{instance}" +msgstr "" + +#, elixir-format +#: lib/mobilizon/email/user.ex:34 +msgid "Mobilizon: Reset your password on %{instance} instructions" +msgstr "" + +#, elixir-format +#: lib/mobilizon_web/templates/email/password_reset.html.eex:1 +#: lib/mobilizon_web/templates/email/password_reset.text.eex:1 +msgid "Password reset" +msgstr "" + +#, elixir-format +#: lib/mobilizon_web/templates/email/registration_confirmation.html.eex:2 +#: lib/mobilizon_web/templates/email/registration_confirmation.text.eex:5 +msgid "You created an account on %{host} with this email address. You are one click away from activating it. If this wasn't you, please ignore this email." +msgstr "" + +#, elixir-format +#: lib/mobilizon_web/templates/email/password_reset.html.eex:2 +#: lib/mobilizon_web/templates/email/password_reset.text.eex:5 +msgid "You requested a new password for your account on %{host}." +msgstr "" diff --git a/priv/gettext/errors.pot b/priv/gettext/errors.pot index 7b2d5ca2..cdaaac62 100644 --- a/priv/gettext/errors.pot +++ b/priv/gettext/errors.pot @@ -7,7 +7,6 @@ ## Run `mix gettext.extract` to bring this file up to ## date. Leave `msgstr`s empty as changing them here as no ## effect: edit them in PO (`.po`) files instead. - ## From Ecto.Changeset.cast/4 msgid "can't be blank" msgstr "" diff --git a/priv/gettext/fr_FR/LC_MESSAGES/default.po b/priv/gettext/fr_FR/LC_MESSAGES/default.po new file mode 100644 index 00000000..f03e281c --- /dev/null +++ b/priv/gettext/fr_FR/LC_MESSAGES/default.po @@ -0,0 +1,63 @@ +## `msgid`s in this file come from POT (.pot) files. +## +## Do not add, change, or remove `msgid`s manually here as +## they're tied to the ones in the corresponding POT file +## (with the same domain). +## +## Use `mix gettext.extract --merge` or `mix gettext.merge` +## to merge POT files into PO files. +msgid "" +msgstr "" +"Language: fr_FR\n" +"Plural-Forms: nplurals=2\n" + +#, elixir-format +#: lib/mobilizon_web/controllers/feed_controller.ex:59 +msgid "%{actor}'s public events feed" +msgstr "" + +#, elixir-format +#: lib/mobilizon_web/templates/email/email.html.eex:8 +#: lib/mobilizon_web/templates/email/email.text.eex:3 +msgid "An email sent by Mobilizon on %{instance}." +msgstr "" + +#, elixir-format +#: lib/mobilizon_web/templates/email/registration_confirmation.html.eex:1 +#: lib/mobilizon_web/templates/email/registration_confirmation.text.eex:1 +msgid "Confirm the email address" +msgstr "" + +#, elixir-format +#: lib/mobilizon_web/templates/email/password_reset.html.eex:3 +#: lib/mobilizon_web/templates/email/password_reset.text.eex:7 +msgid "If you didn't request this, please ignore this email. Your password won't change until you access the link below and create a new one." +msgstr "" + +#, elixir-format +#: lib/mobilizon/email/user.ex:19 +msgid "Mobilizon: Confirmation instructions for %{instance}" +msgstr "" + +#, elixir-format +#: lib/mobilizon/email/user.ex:34 +msgid "Mobilizon: Reset your password on %{instance} instructions" +msgstr "" + +#, elixir-format +#: lib/mobilizon_web/templates/email/password_reset.html.eex:1 +#: lib/mobilizon_web/templates/email/password_reset.text.eex:1 +msgid "Password reset" +msgstr "" + +#, elixir-format +#: lib/mobilizon_web/templates/email/registration_confirmation.html.eex:2 +#: lib/mobilizon_web/templates/email/registration_confirmation.text.eex:5 +msgid "You created an account on %{host} with this email address. You are one click away from activating it. If this wasn't you, please ignore this email." +msgstr "" + +#, elixir-format +#: lib/mobilizon_web/templates/email/password_reset.html.eex:2 +#: lib/mobilizon_web/templates/email/password_reset.text.eex:5 +msgid "You requested a new password for your account on %{host}." +msgstr "" diff --git a/priv/gettext/fr_FR/LC_MESSAGES/errors.po b/priv/gettext/fr_FR/LC_MESSAGES/errors.po new file mode 100644 index 00000000..a1add0c6 --- /dev/null +++ b/priv/gettext/fr_FR/LC_MESSAGES/errors.po @@ -0,0 +1,87 @@ +## `msgid`s in this file come from POT (.pot) files. +## +## Do not add, change, or remove `msgid`s manually here as +## they're tied to the ones in the corresponding POT file +## (with the same domain). +## +## Use `mix gettext.extract --merge` or `mix gettext.merge` +## to merge POT files into PO files. +msgid "" +msgstr "" +"Language: fr_FR\n" +"Plural-Forms: nplurals=2\n" + +msgid "can't be blank" +msgstr "" + +msgid "has already been taken" +msgstr "" + +msgid "is invalid" +msgstr "" + +msgid "must be accepted" +msgstr "" + +msgid "has invalid format" +msgstr "" + +msgid "has an invalid entry" +msgstr "" + +msgid "is reserved" +msgstr "" + +msgid "does not match confirmation" +msgstr "" + +msgid "is still associated with this entry" +msgstr "" + +msgid "are still associated with this entry" +msgstr "" + +msgid "should be %{count} character(s)" +msgid_plural "should be %{count} character(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should have %{count} item(s)" +msgid_plural "should have %{count} item(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should be at least %{count} character(s)" +msgid_plural "should be at least %{count} character(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should have at least %{count} item(s)" +msgid_plural "should have at least %{count} item(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should be at most %{count} character(s)" +msgid_plural "should be at most %{count} character(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "should have at most %{count} item(s)" +msgid_plural "should have at most %{count} item(s)" +msgstr[0] "" +msgstr[1] "" + +msgid "must be less than %{number}" +msgstr "" + +msgid "must be greater than %{number}" +msgstr "" + +msgid "must be less than or equal to %{number}" +msgstr "" + +msgid "must be greater than or equal to %{number}" +msgstr "" + +msgid "must be equal to %{number}" +msgstr "" diff --git a/test/mobilizon_web/controllers/feed_controller_test.exs b/test/mobilizon_web/controllers/feed_controller_test.exs new file mode 100644 index 00000000..92eca1de --- /dev/null +++ b/test/mobilizon_web/controllers/feed_controller_test.exs @@ -0,0 +1,37 @@ +defmodule MobilizonWeb.FeedControllerTest do + use MobilizonWeb.ConnCase + import Mobilizon.Factory + + describe "/@:preferred_username.atom" do + test "it returns an RSS representation of the actor's public events", %{conn: conn} do + actor = insert(:actor) + event1 = insert(:event, organizer_actor: actor) + event2 = insert(:event, organizer_actor: actor) + + conn = + conn + |> put_req_header("accept", "application/atom+xml") + |> get("/@#{actor.preferred_username}.atom") + + assert response(conn, 200) =~ "" + assert response_content_type(conn, :xml) =~ "charset=utf-8" + + {:ok, feed, _} = FeederEx.parse(conn.resp_body) + + assert feed.title == actor.preferred_username <> "'s public events feed" + + Enum.each(feed.entries, fn entry -> + assert entry.title in [event1.title, event2.title] + end) + end + + test "it doesn't return anything for an not existing actor", %{conn: conn} do + conn = + conn + |> put_req_header("accept", "application/atom+xml") + |> get("/@notexistent.atom") + + assert response(conn, 404) == "Not found" + end + end +end